ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Claude Code 错误处理体系:类型安全的错误传播与恢复完整指南(10000 字详解)

Claude Code 错误处理体系:类型安全的错误传播与恢复完整指南(10000 字详解) 1. 为什么 Claude Code 的错误处理不能只靠 try-catch先说结论在 Claude Code 这类工具链里错误处理的核心不是捕获异常而是让错误成为类型的一部分。你写一个工具函数、一次 API 调用、一段插件执行逻辑如果错误只能靠运行时抛出来那调用方永远不知道要处理什么编译器也帮不上忙。我见过太多项目try-catch包了一层又一层最后catch (e)里只能console.log(e)然后返回一个null。调用方拿到null之后继续往下走等到某个字段访问报Cannot read property of undefined才回头找问题。这时候错误已经传播了五六层堆栈信息早就断了。Claude Code 的错误处理体系解决的就是这个问题。它把错误分成几个明确的类型网络错误、API 错误、工具错误、用户错误、配置错误。每种错误有自己的错误码、是否可重试的标记、以及面向用户的友好消息。调用方拿到的是一个ResultT, E要么是成功值要么是错误对象没有第三种状态。这套体系适合谁适合所有用 Claude Code 构建工具链的开发者。你可能是写一个自定义工具让 Claude Code 调用也可能是做一个插件系统或者只是想让自己的 API 调用更稳。只要你的代码里有可能失败的操作这套模式就能用。核心检索词就三个Claude Code、错误处理、类型安全。这三个词贯穿全文。Result 类型是手段重试策略是保障错误边界是隔离。下面我会从类型定义开始一步步给出可复制的代码、配置片段和验证动作。先看一个对比。传统写法async function fetchData(): Promisestring { try { const res await fetch(url) return await res.text() } catch (e) { console.log(e) return null // 调用方不知道这里可能返回 null } }Result 写法async function fetchData(): PromiseResultstring, NetworkError { try { const res await fetch(url) return ok(await res.text()) } catch (e) { return err(new NetworkError(fetch failed, e)) } }区别在哪调用方拿到Resultstring, NetworkError之后TypeScript 会强制你检查ok字段。你不检查编译器就报错。这就是类型安全的价值把记得处理错误从人的自觉变成编译器的强制。2. TaoToken 前置把模型调用接进错误处理链路在讲具体的错误类型定义之前得先把模型调用这一层接进来。因为 Claude Code 的工具链里很多错误最终都来自模型 API 调用。你需要一个稳定的接入点才能让错误传播链路完整。TaoToken 的接入方式很简单。官网是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 端点是 https://taotoken.net/api 。注意 API 地址不加 UTM 参数直接写就行。你需要准备三件套Base URL、API Key、Model ID。这三个东西在后面的配置片段里会反复出现。Base URL 就是https://taotoken.net/api。API Key 在控制台创建地址是 https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 。Model ID 根据你用的模型填比如claude-sonnet-4-20250514这类。如果你用的是 Claude Code 的 coding plan可以在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 看到套餐说明。模型对话的入口在 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。为什么要在错误处理文章里讲接入因为错误传播链路的起点就是 API 调用。你的withRetry函数包的第一个操作大概率就是模型请求。如果接入点不稳定后面的重试策略再漂亮也没用。我试过把 API 调用直接写在业务逻辑里结果就是每个调用点都要写一遍超时、重试、错误转换。后来改成统一走一个callModel函数返回ResultModelResponse, APIError整个链路就清晰了。这里给一个最小的接入配置放在src/config/model.tsexport const modelConfig { baseURL: https://taotoken.net/api, apiKey: process.env.TAOTOKEN_API_KEY ?? , model: claude-sonnet-4-20250514, timeout: 30000, maxRetries: 3, }注意apiKey从环境变量读不要硬编码。timeout设 30 秒maxRetries设 3 次这两个值后面会被重试策略用到。如果你用 Claude Code 的 Anthropic 兼容模式配置在 https://taotoken.net/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaude_codeutm_campaignrewrite 有说明。核心还是那三件套Base URL 填https://taotoken.net/apiKey 填你创建的Model ID 填对应模型。接入完成之后你的模型调用应该返回一个Result类型而不是直接抛异常。这样错误才能进入后面的传播链路。下一节给出完整的类型定义和配置片段。3. 可复制配置Result 类型、错误分类与重试参数这一节是全文的核心所有代码都可以直接复制到项目里。我按文件路径组织你照着建文件就行。3.1 Result 类型定义src/Result.tsexport type ResultT, E Error SuccessT | FailureE export interface SuccessT { readonly ok: true readonly value: T } export interface FailureE { readonly ok: false readonly error: E } export function okT(value: T): ResultT, never { return { ok: true, value } } export function errE Error(error: E): Resultnever, E { return { ok: false, error } }这三个东西是基础。ok和err是两个构造函数Result是联合类型。注意readonly这是为了保证不可变。你拿到一个Result之后不能改它的ok字段只能读。3.2 错误类型定义src/errors/types.tsexport enum ErrorType { USER_ERROR user_error, SYSTEM_ERROR system_error, NETWORK_ERROR network_error, API_ERROR api_error, TOOL_ERROR tool_error, CONFIG_ERROR config_error, UNKNOWN_ERROR unknown_error, } export class AppError extends Error { constructor( message: string, public type: ErrorType, public code: string, public retryable: boolean false, public metadata?: Recordstring, unknown ) { super(message) this.name AppError } getUserMessage(): string { const messages: RecordErrorType, string { [ErrorType.USER_ERROR]: 操作失败请检查您的输入, [ErrorType.SYSTEM_ERROR]: 系统错误请稍后重试, [ErrorType.NETWORK_ERROR]: 网络连接失败请检查网络, [ErrorType.API_ERROR]: 服务暂时不可用请稍后重试, [ErrorType.TOOL_ERROR]: 工具执行失败, [ErrorType.CONFIG_ERROR]: 配置错误请检查配置文件, [ErrorType.UNKNOWN_ERROR]: 发生未知错误, } return messages[this.type] ?? messages[ErrorType.UNKNOWN_ERROR] } } export class NetworkError extends AppError { constructor(message: string, public originalError?: unknown) { super(message, ErrorType.NETWORK_ERROR, NETWORK_ERROR, true) this.name NetworkError } } export class APIError extends AppError { constructor( message: string, public statusCode?: number, public responseBody?: unknown ) { super(message, ErrorType.API_ERROR, API_ERROR, true) this.name APIError } } export class ToolError extends AppError { constructor(message: string, public toolName: string) { super(message, ErrorType.TOOL_ERROR, TOOL_ERROR, false) this.name ToolError } }关键点是retryable字段。网络错误和 API 错误标记为可重试工具错误和用户错误标记为不可重试。这个标记会被重试策略读取决定要不要继续重试。3.3 重试配置src/retry.tsexport interface RetryOptions { maxRetries?: number initialDelay?: number maxDelay?: number backoffMultiplier?: number jitter?: number isRetryable?: (error: unknown) boolean } export async function withRetryT( operation: () PromiseResultT, options: RetryOptions {} ): PromiseResultT { const { maxRetries 3, initialDelay 1000, maxDelay 30000, backoffMultiplier 2, jitter 0.1, isRetryable () true, } options let lastError: unknown null let attempt 0 while (attempt maxRetries) { const result await operation() if (result.ok) return result if (!isRetryable(result.error)) return result lastError result.error const delay calculateBackoffDelay( attempt, initialDelay, maxDelay, backoffMultiplier, jitter ) await sleep(delay) attempt } return err(new Error(Operation failed after ${maxRetries} retries)) } function calculateBackoffDelay( attempt: number, initialDelay: number, maxDelay: number, multiplier: number, jitter: number ): number { const exponential initialDelay * Math.pow(multiplier, attempt) const capped Math.min(exponential, maxDelay) const jitterRange capped * jitter const jitterAmount (Math.random() * 2 - 1) * jitterRange return capped jitterAmount } function sleep(ms: number): Promisevoid { return new Promise((resolve) setTimeout(resolve, ms)) }这段代码里maxRetries默认 3initialDelay默认 1000 毫秒backoffMultiplier默认 2也就是指数退避。jitter默认 0.1加 10% 的随机抖动避免多个请求同时重试造成惊群。3.4 模型调用配置src/config/model.tsexport const modelConfig { baseURL: https://taotoken.net/api, apiKey: process.env.TAOTOKEN_API_KEY ?? , model: claude-sonnet-4-20250514, timeout: 30000, retry: { maxRetries: 3, initialDelay: 1000, maxDelay: 30000, backoffMultiplier: 2, jitter: 0.1, }, }如果你用 Claude Code 的 settings 文件可以写成 JSON{ model: { baseURL: https://taotoken.net/api, apiKey: ${TAOTOKEN_API_KEY}, model: claude-sonnet-4-20250514, timeout: 30000 }, retry: { maxRetries: 3, initialDelay: 1000, maxDelay: 30000, backoffMultiplier: 2, jitter: 0.1 } }注意apiKey用${TAOTOKEN_API_KEY}这种占位符实际运行时从环境变量注入。不要把 Key 写死在配置文件里。3.5 错误边界src/errors/boundary.tsexport interface ErrorContext { operation?: string cacheKey?: string userId?: string metadata?: Recordstring, unknown } export class ErrorBoundary { async executeT( operation: () PromiseT, context?: ErrorContext ): PromiseResultT { try { const value await operation() return ok(value) } catch (error) { console.error([ErrorBoundary], context?.operation, error) return err(error as Error) } } }错误边界的作用是隔离故障。插件执行、工具调用、用户代码都应该包在错误边界里。这样某个插件崩了不会影响主流程。4. 验证请求从调用到成功结果配置写完了得验证。这一节给出完整的验证步骤从发一个请求开始到看到成功结果为止。4.1 写一个最小的验证脚本新建scripts/verify.tsimport { ok, err, Result } from ../src/Result import { withRetry } from ../src/retry import { NetworkError, APIError } from ../src/errors/types import { modelConfig } from ../src/config/model async function callModel(prompt: string): PromiseResultstring { try { const res await fetch(${modelConfig.baseURL}/v1/messages, { method: POST, headers: { Content-Type: application/json, x-api-key: modelConfig.apiKey, anthropic-version: 2023-06-01, }, body: JSON.stringify({ model: modelConfig.model, max_tokens: 128, messages: [{ role: user, content: prompt }], }), }) if (!res.ok) { return err(new APIError(HTTP ${res.status}, res.status)) } const data await res.json() return ok(data.content?.[0]?.text ?? ) } catch (e) { return err(new NetworkError(request failed, e)) } } async function main() { const result await withRetry( () callModel(用一句话说明什么是 Result 类型), modelConfig.retry ) if (result.ok) { console.log(SUCCESS:, result.value) } else { console.error(FAILED:, result.error) } } main()4.2 运行验证先设置环境变量export TAOTOKEN_API_KEY你的Key然后运行npx tsx scripts/verify.ts如果一切正常你会看到类似这样的输出SUCCESS: Result 类型是一种把成功和失败都编码进类型系统的模式调用方必须显式处理两种情况。如果失败你会看到错误对象包含type、code、retryable字段。比如FAILED: APIError { message: HTTP 401, type: api_error, code: API_ERROR, retryable: true, statusCode: 401 }4.3 验证重试逻辑想验证重试是否生效可以故意把 Base URL 改成一个不存在的地址然后观察日志。你会看到类似Retry attempt 1/3, waiting 1000ms... Retry attempt 2/3, waiting 2000ms... Retry attempt 3/3, waiting 4000ms... FAILED: NetworkError三次重试延迟分别是 1 秒、2 秒、4 秒符合指数退避。加上 10% 抖动实际延迟会在 900-1100、1800-2200、3600-4400 毫秒之间波动。4.4 验证错误边界再写一个测试故意让一个操作抛异常const boundary new ErrorBoundary() const result await boundary.execute(async () { throw new Error(boom) }, { operation: test }) console.log(result.ok) // false console.log(result.error.message) // boom错误边界捕获了异常返回了Failure。主流程不会崩。4.5 验证成功结果的类型收窄TypeScript 的类型收窄是这套体系的关键。写一段代码const result await callModel(hello) if (result.ok) { // 这里 result 被收窄为 Successstring console.log(result.value.toUpperCase()) } else { // 这里 result 被收窄为 FailureError console.log(result.error.message) }如果你在if (result.ok)分支里访问result.errorTypeScript 会报错。这就是类型安全的价值。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth这一节列出实际开发中最容易遇到的几个报错给出原因和排查步骤。5.1 401 Unauthorized报错长这样APIError: HTTP 401 type: api_error code: API_ERROR retryable: true statusCode: 401原因通常是 API Key 没设置、设置错了、或者环境变量没读到。排查步骤第一步确认环境变量存在echo $TAOTOKEN_API_KEY如果输出为空说明没设置。第二步确认 Key 没有多余空格。第三步确认请求头里的字段名正确。Anthropic 兼容模式用x-api-keyOpenAI 兼容模式用Authorization: Bearer。注意 401 被标记为retryable: true但实际重试没用因为 Key 错了重试一百次还是错。你可以在isRetryable里加判断isRetryable: (error) { if (error instanceof APIError error.statusCode 401) return false return true }5.2 local proxy failed报错长这样NetworkError: local proxy failed type: network_error code: NETWORK_ERROR retryable: true这个错误通常出现在本地开发环境请求发不出去。排查步骤第一步确认 Base URL 写对了。应该是https://taotoken.net/api不要多写斜杠或少写路径。第二步确认本地网络能访问外网。第三步检查是否有本地代理配置干扰。如果你在环境变量里设了HTTP_PROXY或HTTPS_PROXY先清掉unset HTTP_PROXY unset HTTPS_PROXY第四步用 curl 直接测curl -X POST https://taotoken.net/api/v1/messages \ -H x-api-key: $TAOTOKEN_API_KEY \ -H anthropic-version: 2023-06-01 \ -H Content-Type: application/json \ -d {model:claude-sonnet-4-20250514,max_tokens:32,messages:[{role:user,content:hi}]}如果 curl 能通说明是代码问题。如果 curl 也不通说明是网络或配置问题。5.3 reading choices报错长这样TypeError: Cannot read properties of undefined (reading choices)这个错误说明你在解析响应时假设了响应结构里有choices字段但实际响应里没有。原因通常是第一请求失败了返回的是错误对象不是正常的响应。第二你用的 API 格式和响应格式不匹配。Anthropic 格式返回content数组OpenAI 格式返回choices数组。排查步骤先打印原始响应const data await res.json() console.log(JSON.stringify(data, null, 2))看清楚结构再解析。不要假设。正确的解析方式if (!res.ok) { return err(new APIError(HTTP ${res.status}, res.status, await res.text())) } const data await res.json() const text data.content?.[0]?.text ?? data.choices?.[0]?.message?.content ?? return ok(text)用可选链?.和空值合并??避免访问 undefined 的属性。5.4 OAuth 相关错误报错长这样Error: OAuth token expired type: config_error code: CONFIG_ERROR retryable: false如果你用 Claude Code 的 OAuth 登录方式token 过期后会报这个。排查步骤第一步重新登录刷新 token。第二步确认配置文件里的 token 字段没写错。第三步如果用的是 API Key 模式检查是不是误用了 OAuth 配置。在 Claude Code 的 settings 里API Key 模式和 OAuth 模式是互斥的。如果你同时配了可能会冲突。建议统一用 API Key 模式配置简单不容易出错。5.5 错误排查对照表报错类型可重试首要排查点401 UnauthorizedAPI_ERROR否API Key 是否正确local proxy failedNETWORK_ERROR是Base URL 和网络reading choicesTypeError否响应结构解析OAuth token expiredCONFIG_ERROR否认证模式配置这张表可以贴在工位上遇到报错先对号入座。6. 把错误处理接进你的 Claude Code 工具链前面五节讲完了类型定义、配置、验证和排查。这一节说怎么把这套东西真正用起来。第一步把Result类型和错误类复制到你的项目里。路径按你的项目结构调整但文件名建议保持一致方便对照。第二步把所有可能失败的函数改成返回Result。包括 API 调用、文件读写、工具执行、插件加载。改的时候不要急一个函数一个函数改改完跑一遍测试。第三步在调用点用if (result.ok)收窄类型。不要用result.value!这种非空断言那等于放弃了类型安全。第四步给可重试的操作包上withRetry。重试参数从配置文件读不要硬编码。第五步给插件和工具调用包上ErrorBoundary。隔离故障防止一个插件崩了影响全局。如果你用 Claude Code 的 coding plan可以在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 看到长期编码场景的配置建议。模型对话的调试入口在 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。API Key 管理在 https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 建议给不同的项目创建不同的 Key方便排查问题。最后说一个实际经验。我一开始觉得Result类型很啰嗦每个调用点都要写if (result.ok)。但用了两周之后发现正是这个啰嗦让我少写了很多 bug。以前那种try-catch然后返回null的写法看起来简洁实际上把问题推迟到了运行时。现在编译器帮你检查反而省心。如果你在接入过程中遇到问题可以先看接入文档再对照第五节的排查表。大部分错误都能定位到具体原因。
返回列表