Analysis | 错误处理模式全拆¶
目的:理解 Claude Code 的错误处理哲学和具体模式。 关联:topics/async-generator-pattern.md 的 throw vs yield-error
1. 错误处理哲学¶
1.1 "错误是事件,不是异常"¶
Claude Code 的错误处理核心思想:
不要"抛异常就崩溃",要"分类错误,分类处理"。
// ❌ 传统
try {
const data = await fetch(url)
return data
} catch (err) {
throw new Error('Failed to fetch') // 抛给上层
}
// ✅ Claude Code 风格
const result = await withRetry(() => fetch(url), {
classifyError: (err) => {
if (err.status === 429) return { retryable: true, backoff: 1000 }
if (err.status === 401) return { retryable: false, code: 'AUTH' }
if (err.status >= 500) return { retryable: true, backoff: 5000 }
return { retryable: false, code: 'UNKNOWN' }
},
})
1.2 "四类错误 + 四类处理"¶
| 错误类 | 例子 | 处理 |
|---|---|---|
| 可重试 | 429 限流、5xx 服务端错误、网络断开 | withRetry(指数退避) |
| 不可重试 - 用户错误 | 400 请求无效、参数错误 | 立即返回 + 用户提示 |
| 不可重试 - 鉴权错误 | 401 认证失败、403 权限不足 | 触发登录流程 / 提示用户 |
| 不可重试 - 系统错误 | 程序 bug、配置错误 | 报错 + 上报 telemetry |
2. 错误分类实现¶
2.1 categorizeRetryableAPIError(推测)¶
// src/services/api/errors.ts
export function categorizeRetryableAPIError(err: Error): RetryableError | null {
// 1. 网络错误
if (err instanceof NetworkError) {
return {
retryable: true,
backoff: 1000,
reason: 'network',
}
}
// 2. HTTP 状态码
const status = (err as any).status
if (status === 429) {
return {
retryable: true,
backoff: parseRetryAfter(err), // 读 Retry-After header
reason: 'rate_limit',
}
}
if (status === 408 || status === 504) {
return {
retryable: true,
backoff: 2000,
reason: 'timeout',
}
}
if (status >= 500 && status < 600) {
return {
retryable: true,
backoff: 5000,
reason: 'server_error',
}
}
// 3. 不可重试
if (status === 400) return { retryable: false, reason: 'bad_request' }
if (status === 401) return { retryable: false, reason: 'auth' }
if (status === 403) return { retryable: false, reason: 'forbidden' }
if (status === 404) return { retryable: false, reason: 'not_found' }
return null // 未知错误
}
2.2 withRetry 实现(推测)¶
// src/services/api/withRetry.ts
export async function* withRetry<T>(
fn: () => AsyncGenerator<T>,
options: RetryOptions
): AsyncGenerator<T> {
let attempt = 0
const maxAttempts = options.maxAttempts ?? 5
while (true) {
try {
for await (const event of fn()) {
yield event // 透传
}
return // 成功
} catch (err) {
const classification = options.classifyError(err as Error)
if (!classification?.retryable) {
throw err // 不可重试
}
attempt++
if (attempt >= maxAttempts) {
throw new MaxRetriesExceededError(err, attempt)
}
// 指数退避
const backoff = classification.backoff * Math.pow(2, attempt - 1)
await sleep(backoff)
}
}
}
3. 自定义 Error 类¶
// src/services/api/errors.ts(推测)
export class FallbackTriggeredError extends Error {
constructor(public originalError: Error, public fallbackModel: string) {
super(`Fallback to ${fallbackModel}: ${originalError.message}`)
}
}
export class MaxRetriesExceededError extends Error {
constructor(public originalError: Error, public attempts: number) {
super(`Failed after ${attempts} attempts: ${originalError.message}`)
}
}
export class ImageSizeError extends Error {
constructor(public width: number, public height: number, public maxSize: number) {
super(`Image ${width}x${height} exceeds max ${maxSize}`)
}
}
export class ImageResizeError extends Error {
constructor(public reason: string) {
super(`Image resize failed: ${reason}`)
}
}
特点:
- 类名带 Error 后缀
- 携带额外字段(attempts / model / size)
- 支持 instanceof 检查
4. 错误边界(Error Boundary)¶
4.1 React Error Boundary¶
// src/components/ErrorBoundary.tsx(推测)
class ErrorBoundary extends React.Component<Props, State> {
state = { hasError: false, error: null as Error | null }
static getDerivedStateFromError(error: Error) {
return { hasError: true, error }
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
// 1. 上报 telemetry
diagnosticTracker.reportError(error, errorInfo)
// 2. 恢复策略
this.recoverFromError(error)
}
render() {
if (this.state.hasError) {
return <ErrorOverview error={this.state.error!} />
}
return this.props.children
}
}
4.2 Ink 错误 UI¶
// src/ink/components/ErrorOverview.tsx
function ErrorOverview({ error }: { error: Error }) {
return (
<Box flexDirection="column">
<Text color="red">⚠ An error occurred</Text>
<Text>{error.message}</Text>
{error.stack && (
<Box flexDirection="column" marginTop={1}>
<Text dimColor>Stack trace:</Text>
{error.stack.split('\n').map((line, i) => (
<Text key={i} dimColor> {line}</Text>
))}
</Box>
)}
</Box>
)
}
5. 错误恢复策略¶
5.1 "重试 + 降级"¶
// 推测的主 LLM 调用流程
async function* streamWithFallback(messages) {
try {
yield* withRetry(() => streamApi(messages, primaryModel), classifyApiError)
} catch (err) {
// 降级到次要模型
if (err.status === 429 || err.status === 529) {
yield* withRetry(() => streamApi(messages, fallbackModel), classifyApiError)
} else {
throw err
}
}
}
5.2 "优雅降级"¶
// 推测的权限检查失败降级
async function checkPermissionWithFallback(tool, input, ctx) {
try {
return await checkPermissionViaCloud(tool, input, ctx)
} catch (err) {
if (isNetworkError(err)) {
// 离线时用本地缓存
return checkPermissionFromLocalCache(tool, input, ctx)
}
throw err
}
}
5.3 "保存现场 + 重启"¶
// 推测的 crash 恢复
async function gracefulRestart() {
// 1. 保存当前 state 到磁盘
await saveSessionState(getSessionId(), getAppState())
// 2. 退出
process.exit(0)
// 3. 启动时检测未完成会话
// → 自动 /resume
}
6. 错误处理在 agent 循环中的位置¶
// src/query.ts(简化)
async function* queryLoop(messages) {
while (true) {
try {
// 1. 调 LLM(带重试)
for await (const event of withRetry(() => streamApi(messages))) {
yield event
}
} catch (err) {
// 2. 不可重试:注入错误消息
yield {
type: 'error',
error: formatError(err),
}
yield {
type: 'text',
text: 'API error occurred, please retry',
}
return // 结束当前轮
}
// 3. 处理 tool_use
if (hasToolUse) {
for (const tool of toolUses) {
try {
const result = await executeTool(tool, ctx)
messages = [...messages, result]
} catch (err) {
// 4. 工具失败:注入 tool_result with is_error
messages = [...messages, {
type: 'tool_result',
tool_use_id: tool.id,
content: `Tool failed: ${err.message}`,
is_error: true,
}]
}
}
}
}
}
关键: - 任何错误都不"panic" - LLM 错误 → 注入消息 + 结束 - 工具错误 → 注入 tool_result with is_error - LLM 下一轮会"看到"这些错误并调整
7. 错误信息的用户友好化¶
7.1 模式¶
// ❌ 错的:技术错误直接给用户
error.message // "ECONNREFUSED 127.0.0.1:443"
// ✅ 对的:人类可读 + 建议
{
title: '无法连接到 Claude API',
detail: '网络连接失败,请检查你的网络。',
suggestions: [
'检查网络连接',
'如果使用代理,设置 HTTPS_PROXY',
'重试命令',
],
technical: 'ECONNREFUSED 127.0.0.1:443', // 折叠显示
}
7.2 实际渲染¶
function renderError(err: UserFacingError) {
return (
<Box flexDirection="column" borderColor="red" borderStyle="round">
<Text color="red">⚠ {err.title}</Text>
<Text>{err.detail}</Text>
{err.suggestions && (
<Box flexDirection="column" marginTop={1}>
<Text dimColor>建议:</Text>
{err.suggestions.map((s, i) => (
<Text key={i}> {i + 1}. {s}</Text>
))}
</Box>
)}
{err.technical && (
<Box marginTop={1}>
<Text dimColor>技术细节: {err.technical}</Text>
</Box>
)}
<Box marginTop={1}>
<KeyboardShortcutHint keys="Enter" label="重试" />
<KeyboardShortcutHint keys="Esc" label="取消" />
</Box>
</Box>
)
}
8. 错误日志 + 上报¶
8.1 logError / logAntError / logForDebugging¶
// src/utils/log.ts(推测)
export function logError(err: Error, context?: Record<string, unknown>): void {
// 1. 写到 ~/.claude/logs/error.log
fs.appendFileSync('~/.claude/logs/error.log', formatError(err, context))
// 2. 控制台输出
console.error(chalk.red(`[ERROR] ${err.message}`))
}
export function logAntError(err: Error, context?: Record<string, unknown>): void {
// Ant 内部:上报到 Sentry + Datadog
Sentry.captureException(err, { contexts: { custom: context } })
Datadog.log('error', err.message, context)
}
8.2 internalLogging¶
// src/services/internalLogging.ts(推测)
export class InternalLogger {
// 1. 内存缓冲
private buffer: LogEntry[] = []
// 2. 定期 flush
setInterval(() => this.flush(), 30_000)
// 3. flush 到 telemetry
private async flush() {
if (this.buffer.length === 0) return
await sendToTelemetry(this.buffer.splice(0))
}
// 4. 退出前 flush
onExit(() => this.flush())
}
9. 错误处理的关键洞察¶
9.1 "错误是事件"¶
async function* 让错误可以作为事件 yield:
9.2 "分类 + 决策"¶
不抛异常就完事。每个错误都有"分类"和"决策": - 可重试?等多久? - 降级?降级到哪? - 通知用户?显示什么?
9.3 "用户友好化"¶
技术错误 ≠ 用户看到的错误。
永远有"建议"(不只是"失败")。
9.4 "现场保存"¶
会话状态定期持久化,crash 后能恢复。
9.5 "Error Boundary"¶
React 标准模式。Ink 也有对应实现。
单组件崩溃不毁整个 REPL。
9.6 "可观测性"¶
- logError 写文件
- 内部 logger 缓冲
- 退出前 flush
- Sentry / Datadog 上报
没有可观测性 = 没法改进。
10. 实战:写一个错误处理工具¶
// utils/errorHandling.ts
export class UserFacingError extends Error {
constructor(
public title: string,
public detail: string,
public suggestions: string[] = [],
public technical?: string,
) {
super(detail)
}
}
export function wrapAsUserFacing(
err: unknown,
title: string,
detail: string,
suggestions: string[] = []
): UserFacingError {
if (err instanceof UserFacingError) return err
return new UserFacingError(
title,
detail,
suggestions,
err instanceof Error ? err.message : String(err),
)
}
// 用法
try {
await fetch(url)
} catch (err) {
throw wrapAsUserFacing(
err,
'无法连接到服务',
'请检查你的网络连接',
[
'检查网络',
'检查代理设置',
'重试命令',
],
)
}
11. 阅读清单¶
- ✅
src/services/api/errors.ts(错误分类) - ✅
src/services/api/withRetry.ts(重试) - ✅
src/services/api/claude.ts(错误处理实战) - 📌
src/utils/log.ts(日志) - 📌
src/services/internalLogging.ts(内部日志) - 📌
src/ink/components/ErrorOverview.tsx(错误 UI) - 📌
src/components/ErrorBoundary.tsx(错误边界)
12. 练习任务¶
- 设计你的重试策略 —— 给一个具体场景写分类函数
- 写一个 UserFacingError 包装 —— 包装 fetch / file read / API call
- 画错误流图 —— 一次 API 失败经过的所有节点
- 思考:在 React 里抛异常 vs yield 错误事件,哪个好?Claude Code 选 yield 事件的理由是什么?