一、错误分类和分级
在Hooks系统中,错误并非铁板一块——不同的错误需要不同的处理策略。科学地对错误进行分类和分级,是构建稳健错误处理体系的第一步。
1.1 按可恢复性分类
根据错误是否能够通过重试或等待自行恢复,可将错误分为两大类:
| 分类 | 说明 | 典型示例 | 处理策略 |
| 可恢复错误 | 暂时性故障,重试后可能成功 | 网络超时、临时文件锁、服务暂时不可用 | 自动重试(指数退避) |
| 不可恢复错误 | 永久性故障,重试无法解决 | 配置文件格式错误、依赖缺失、权限不足 | 立即失败,报告用户 |
最佳实践:在catch块中判断错误类型。对于网络相关的Error(如ETIMEDOUT、ECONNRESET),优先标记为可恢复;对于语法错误或模块缺失错误,直接标记为不可恢复。
1.2 按严重程度分级
根据错误对系统安全性和功能完整性的影响,划分为三个级别:
| 级别 | 名称 | 影响范围 | 典型场景 | 处理动作 |
| 关键错误 | CRITICAL | 安全或合规 | 安全违规、密钥泄露、注入攻击 | 立即阻断执行,通知用户 |
| 非关键错误 | WARNING | 功能降级 | 日志写入失败、非核心Hook执行失败 | 记录日志,跳过Hook,继续执行 |
| 信息提示 | INFO | 无影响 | 不必要的文件未找到、缓存未命中 | 仅记录,不中断流程 |
重要:涉及安全凭据的错误应始终划为关键错误。即使该错误看似"可恢复",只要触及凭据(token、API key、密钥文件),都必须立即阻断并通知用户检查凭据状态。
1.3 错误分类的代码实现
在Hooks中通过try-catch捕获错误后,使用分类函数对错误进行判定:
// 错误分类与分级
function classifyError(error) {
const recoverableCodes = ['ETIMEDOUT', 'ECONNRESET', 'EAGAIN'];
const criticalPatterns = [/token/i, /secret/i, /key/i, /credential/i];
return {
isRecoverable: recoverableCodes.includes(error.code),
isCritical: criticalPatterns.some(p => p.test(error.message)),
severity: error.code === 'ENOENT' ? 'WARNING' : 'ERROR',
timestamp: new Date().toISOString()
};
}
二、优雅降级策略
优雅降级(Graceful Degradation)是Hook错误处理的核心思想——当某个Hook执行失败时,系统不应整体崩溃,而是以"降级模式"继续运行,尽可能保证核心功能的可用性。
2.1 before Hook失败时的降级
before Hook在任务执行前运行,如果其检查失败,应根据检查的关键性决定行为:
- 关键检查失败(如安全校验):必须阻断执行,并给出明确错误信息
- 非关键检查失败(如缓存预热、预加载):发出warning,允许继续执行
// before Hook 的优雅降级
beforeHook(hookContext) {
try {
return await this.runChecks(hookContext);
} catch (error) {
const classification = classifyError(error);
if (classification.isCritical) {
throw new HookBlockingError('安全校验失败,已阻断执行', error);
}
console.warn(`[非关键Hook失败] ${error.message} — 已降级跳过`);
return { skipped: true, reason: error.message };
}
}
设计原则:before Hook降级时,应始终给用户明确的反馈——告知哪一步被跳过以及原因。静默跳过会让用户困惑,影响调试体验。
2.2 after Hook失败时的降级
after Hook在任务完成后执行,原则上不应影响主流程的返回结果:
- after Hook失败时,主流程的结果已经产生
- 应确保after Hook的任何异常都不会传播到调用方
- 记录错误日志,供事后分析
// after Hook 的优雅降级(不传播异常)
afterHook(result, hookContext) {
try {
await this.postProcess(result, hookContext);
} catch (error) {
console.error(`[afterHook失败] ${error.message} — 不影响主流程`);
// 绝不重新throw异常
}
}
设计原则:after Hook应像"消防演习"——即使失败也不影响已经完成的建筑(主流程)。始终在after Hook的最外层catch中消化所有异常,防止未捕获的错误上浮。
2.3 降级后的默认行为
当Hook发生降级时,系统需要有一个明确的"默认动作",避免处于不确定状态:
| Hook类型 | 降级默认行为 | 说明 |
| before Hook | 允许执行(非关键)/ 阻断(关键) | 非关键检查失败时,以warning放行 |
| after Hook | 允许完成 | 不影响已产出的结果 |
| transform Hook | 返回原始数据 | 放弃转换,使用未处理的原始输入 |
| filter Hook | 放行条目 | 过滤失败时不误杀,宁放过勿错杀 |
核心原则:降级的默认行为应遵循"最小意外原则"(Principle of Least Astonishment)。用户应当始终获得可预期的行为,而不是在Hook失败后得到令人困惑的错误状态。
三、错误重试机制
对于可恢复的错误,自动重试是最有效的处理手段。合理的重试策略可以在不打扰用户的情况下自愈大部分临时故障。
3.1 指数退避策略
指数退避(Exponential Backoff)通过逐次增加重试间隔,避免对下游系统造成"重试风暴":
// 指数退避重试实现
async function retryWithBackoff(fn, options = {}) {
const {
maxRetries = 3,
baseDelay = 1000, // 初始1秒
maxDelay = 30000, // 最大30秒
jitter = true // 添加随机抖动
} = options;
let lastError;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
if (attempt === maxRetries) break;
const delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
const actualDelay = jitter ? delay * (0.5 + Math.random() * 0.5) : delay;
console.log(`[重试] 第${attempt + 1}次尝试失败,${Math.round(actualDelay)}ms后重试...`);
await new Promise(resolve => setTimeout(resolve, actualDelay));
}
}
throw new RetryExhaustedError(`重试 ${maxRetries} 次后仍然失败`, lastError);
}
重试时间线(baseDelay=1s, maxRetries=3):
* 第1次失败 → 等待 1.0s(1s * 2^0)
* 第2次失败 → 等待 2.0s(1s * 2^1)
* 第3次失败 → 等待 4.0s(1s * 2^2)
* 第4次失败 → 抛出RetryExhaustedError,不再重试
抖动(Jitter):在实际延迟上增加±25%的随机抖动,防止多个客户端同时重试造成"惊群效应"。
3.2 最大重试次数限制
无限重试会导致资源耗尽和级联故障。必须为每种Hook设置合理的最大重试次数:
| 策略名称 | 最大重试次数 | 适用场景 |
| 默认策略 | 3次 | 大多数网络错误场景 |
| 保守策略 | 5次 | 长时间运行的后台任务 |
| 激进策略 | 1次 | 对延迟敏感的交互式Hook |
| 不重试 | 0次 | 关键安全检查和不可恢复错误 |
注意:最大重试次数应作为配置项暴露给用户,并附带合理的默认值。不同场景下用户可能期望不同的重试行为。
3.3 重试成功后的状态恢复
重试成功并不意味着一切恢复如初——还需要做"后处理"来恢复系统状态:
// 重试成功后的状态恢复
async function executeWithRecovery(hookFn, context) {
const result = await retryWithBackoff(() => hookFn(context));
if (result.recovered) {
// 清除错误状态标记
context.errorCount = 0;
context.lastError = null;
context.recoveryTime = new Date().toISOString();
console.log('[恢复] Hook已从失败状态中恢复');
}
return result;
}
四、错误通知和告警
及时的错误通知和告警机制,可以帮助开发者快速发现并处理问题,避免小问题发酵成大故障。
4.1 分层通知策略
根据错误的严重程度和发生频率,采用不同的通知方式:
| 通知级别 | 触发条件 | 通知方式 | 示例 |
| 紧急 | 关键错误 / 连续失败 > 5次 | 弹窗通知 + 声音告警 | 密钥泄露、安全违规 |
| 警告 | 同一Hook 1分钟内失败 > 3次 | 状态栏通知 | API限流、文件锁冲突 |
| 提示 | 单次非关键失败 | 日志记录 | 缓存未命中、可选依赖缺失 |
4.2 告警升级机制
当错误持续发生时,告警级别应自动升级,确保严重问题不会被淹没在海量低级别告警中:
// 错误告警升级
class AlertManager {
constructor() {
this.failureCounts = new Map();
this.alertLevels = ['INFO', 'WARNING', 'CRITICAL'];
}
recordFailure(hookName, error) {
const count = (this.failureCounts.get(hookName) || 0) + 1;
this.failureCounts.set(hookName, count);
if (count >= 5) {
return this.sendAlert('CRITICAL', hookName, error);
} else if (count >= 3) {
return this.sendAlert('WARNING', hookName, error);
}
return this.sendAlert('INFO', hookName, error);
}
sendAlert(level, hookName, error) {
console.log(`[${level}] Hook "${hookName}" 失败: ${error.message}`);
// 可扩展为发送到外部告警系统(PagerDuty、Slack等)
}
}
4.3 多渠道通知
错误信息应通过多个渠道传递,确保重要告警不遗漏:
- 控制台日志:近实时查看,适合开发和调试阶段
- 文件日志:持久化存储到
~/.claude/hooks/error.log
- 用户通知:通过Claude Code的状态栏或通知区域展示
- 外部系统:发送到Sentry、Datadog等监控平台(可选)
告警策略要点:避免"告警疲劳"——对重复错误进行聚合,同一类型的错误在短时间内只发一条通知。同时为告警设置抑制窗口(如5分钟内同一Hook不再重复触发同一级别的告警),确保用户不会被信息轰炸。
五、错误日志和事后分析
完善的错误日志是排查问题的"黑匣子"。好的日志不仅记录发生了什么,还提供足够的上下文帮助快速定位根因。
5.1 完整的错误上下文
每条错误日志应包含以下信息,形成完整的可追溯记录:
// 结构化错误日志
function logHookError(hookName, error, context) {
const logEntry = {
timestamp: new Date().toISOString(),
hook: hookName,
errorMessage: error.message,
errorCode: error.code,
stackTrace: error.stack,
recoverable: classifyError(error).isRecoverable,
attempt: context.attempt || 1,
sessionId: context.sessionId,
systemInfo: {
platform: process.platform,
nodeVersion: process.version
}
};
appendToLogFile('hooks-error.log', logEntry);
}
5.2 错误分类聚合分析
定期对日志中的错误进行聚合分析,识别高频错误模式,为系统优化提供数据支撑:
// 错误聚合分析
async function analyzeErrorPatterns() {
const logs = await readErrorLogs();
const grouped = logs.reduce((acc, entry) => {
const key = `${entry.hook}:${entry.errorCode}`;
acc[key] = acc[key] || { count: 0, entries: [] };
acc[key].count++;
acc[key].entries.push(entry);
return acc;
}, {});
console.table(
Object.entries(grouped)
.map(([key, val]) => ({ pattern: key, count: val.count }))
.sort((a, b) => b.count - a.count)
);
}
5.3 常见错误模式与解决方案库
建立常见错误的知识库,方便快速查找解决方案,提升排查效率:
| 错误模式 | 错误码 | 可能原因 | 推荐方案 |
| 网络超时 | ETIMEDOUT | 网络不稳定、代理配置错误 | 启用重试(指数退避),检查代理设置 |
| 文件不存在 | ENOENT | 路径错误、文件被删除 | 检查文件路径,提供默认值 |
| 权限拒绝 | EACCES | 文件权限不足 | 检查读写权限,建议使用用户目录 |
| 模块未找到 | MODULE_NOT_FOUND | 依赖未安装 | 检查package.json,运行安装命令 |
| 语法错误 | SyntaxError | Hook脚本中有语法错误 | 检查Hook脚本语法,添加单元测试 |
注意:对每种常见错误模式,应编写对应的自动化恢复脚本。例如,遇到 ENOENT 时自动创建缺失目录,遇到 ECONNRESET 时自动重试连接。自动化恢复是降低MTTR的最有效手段。
5.4 SLA监控指标
通过量化指标衡量Hook错误处理体系的有效性:
关键SLA指标:
* Hook成功率:≥ 99.9%(非关键Hook合计)
* 关键Hook成功率:100%(任何一次失败都必须人工介入)
* 自动恢复率:≥ 95%(通过重试成功恢复的比例)
* 平均恢复时间(MTTR):< 5秒
* 告警响应时间:紧急告警 < 1分钟,普通告警 < 5分钟
"错误处理的最高境界不是永不犯错,而是犯错后能优雅降级、快速恢复、持续改进。" —— 系统稳定性箴言