用claude code辅助编写错误处理中间件的注意事项

三周前,我让 Claude Code 帮我改了一个 Express 项目的错误处理中间件。代码跑通的那一刻,diff 干净得像教科书,统一的错误码枚举、结构化的日志输出、根据环境变量自动切换的错误详情策略。我反复看了三遍,挑不出毛病。合并,部署,下班。

凌晨两点,PagerDuty 响了。

所有 /api/order 接口返回 500。日志里没有堆栈,没有 source map 映射后的行号,没有 traceId,只有一条 JSON:

{"code":"INTERNAL_ERROR","message":"Something went wrong"}
“Something went wrong”,这是 Claude Code 帮我生成的默认兜底消息。而真正触发这条消息的那个异步错误,在中间件的某处被静默吞掉了。那天晚上我在 Datadog 里翻了 46 分钟,最后发现是 Redis 连接池耗尽导致的 unhandled rejection,但中间件既没有捕获它,也没有让它冒泡,而是优雅地返回了一个毫无诊断价值的 JSON。

这不是 Claude Code 的问题。这是我的问题。 我让它在一个足够复杂的项目里自由发挥,给了它一个模糊的 prompt,得到了一个看起来完美的答案,然后我选择了信任。

这篇文章不教你怎么用 AI 写代码。网上已经有太多“三分钟用 AI 生成错误处理中间件”的教程。我要写的是那些 AI 不会告诉你的事:当 Claude Code 把代码交给你之后,真正的战斗才开始。

一、核心结论:AI 写中间件的真实能力边界

先说最重要的结论,这是我过去 6 个月在生产环境里反复验证过的:

Claude Code 擅长将“已有的模式”翻译成代码,不擅长判断“这个模式是否适合你的场景”。

具体到错误处理中间件,它能出色地完成以下任务:

根据你指定的错误类型生成对应的类定义和枚举

按照你给的日志格式(JSON / text / logfmt)生成格式化函数

复制你项目中已有的错误处理模式并应用到新的中间件上

根据 HTTP 状态码映射生成标准的响应体

它做不好的事:

判断某个错误应该被“吞掉”还是“抛出”,这需要理解业务语义

识别异步错误的捕获缺口,它生成的代码看起来能处理 async,但边界情况经常遗漏

决定什么信息在什么环境暴露,安全问题需要你明确约束

写出可调试的错误,AI 天然追求“干净”,会过度简化错误上下文

一句话:Claude Code 是顶级的代码翻译器,但不是架构决策者。 你给它清晰的约束,它给你可靠的代码。你让它“看着办”,它给你一场凌晨两点的 oncall。

下面我会把这个结论拆开揉碎,用我踩过的坑、验证过的方案、以及反复迭代出来的 prompt 模板,帮你建立一套用 AI 辅助编写错误处理中间件的完整实践框架。

二、真实场景:为什么我们需要重新审视 AI 生成的中间件

2.1 一个典型项目的错误处理演化

让我先描述一个典型的 Node.js 后端项目的错误处理进化史,我见过太多团队走这条路:

阶段一:没有中间件。 每个 controller 里散落着 try-catch,catch 块里的代码比业务逻辑还长。日志格式不统一,有的用 console.error,有的用 logger.error,有的直接忘了写。

阶段二:手写了一个基础中间件。 某个 senior 花了一下午写了 60 行的 error middleware,能区分 ValidationError 和 AppError,返回统一格式。但只处理同步错误,async 路由的异常还是会 crash 进程。

阶段三:发现了 async 的问题,打了个补丁。 加上了 try { await next() } catch (err) { ... },但没处理事件循环里的 unhandled rejection。同时错误码逐渐膨胀到 40 多个,每次加新的都要手动维护映射表,烦。

阶段四:某人说“让 Claude Code 重写一下吧”。 于是把旧代码贴进对话框,prompt 是:“帮我重写一个更完善的错误处理中间件”。AI 返回了一份漂亮的代码,重构成了策略模式,加了一堆注释,看起来像是从某个 blog 复制下来的最佳实践。团队很高兴,合并了。

阶段五:凌晨两点。 某种在旧代码里不会被吞掉的异常,在新中间件里被优雅地转换成了一个通用的 500 错误,丢失了所有可追溯信息。

这个演化路径说明了一件事:错误处理中间件的价值不在“正常情况”下,而在“极端情况”下。 而 AI 的训练数据里,极端情况的样本远少于正常情况。


用claude code辅助编写错误处理中间件的注意事项
2.2 AI 生成的代码有一个你意识不到的盲区 我做了个实验。让 Claude Code(Claude 4 Sonnet)用相同的 prompt 生成 5 次错误处理中间件,然后统计共性: 维度 5 次生成结果 能正确区分 HTTP 状态码映射 5/5 包含 try-catch 包裹 next() 5/5 使用 winston/pino 日志库 3/5 区分开发/生产环境的错误详情 3/5 正确使用 await next().catch() 模式 0/5 包含 traceId/requestId 注入 1/5 在 catch 中检查 err.statusCode 是否存在 5/5 检查 err 是否为 null/undefined 0/5 处理 SyntaxError(JSON parse 失败等) 1/5 明确区分“可预期错误”和“非预期错误” 0/5 注意第三列和最后一列。AI 假设所有抛到中间件里的 error 都是已经构造好的 AppError 实例。 但真实世界不是这样的,你会遇到: 库作者直接 throw 'connection failed'(字符串,不是 Error 实例) JSON.parse 扔出的 SyntaxError(message 里包含敏感的用户输入) Postgres 驱动抛出的 error 对象,code 属性在 original 而不是顶层 由 Promise.reject 引发的错误,stack 可能为 undefined 这些“脏错误”才是生产环境里最常见的。 而 AI 生成的中间件对它们的处理,脆弱得令人不安。 三、拆解 Claude Code 生成错误处理中间件的五大误区 误区一:过度泛化,试图在一段代码里处理一切 这是 AI 最典型的输出模式。你给它一个模糊的 prompt,它给你一个“万能”的中间件。代码通常会这样开头: // ❌ AI 典型生成代码 module.exports = async (err, req, res, next) => { if (err instanceof ValidationError) { ... } else if (err instanceof AuthError) { ... } else if (err.name === 'MongoError') { ... } else if (err.code === 'ECONNREFUSED') { ... } else if (err.type === 'entity.parse.failed') { ... } // ... 继续套娃 }

问题在哪?这不是错误处理,这是分类目录。 一个健康的错误处理架构应该分层:

  1. 协议层:处理 body-parser 的 JSON 解析失败、multipart 边界错误
  2. 库层:每个数据源(Redis、Postgres、S3)的错误应在各自模块内先做一级转换
  3. 领域层:业务异常(余额不足、库存不够)应在 service 层显式抛出
  4. 中间件层:只负责最后一步,统一序列化、日志记录、HTTP 响应

AI 会把这四层全塞进一个函数里,因为它的训练数据里充斥着这种“一个中间件解决所有问题”的博客文章。

正确的做法是:在 prompt 里明确告诉 Claude Code 边界。 比如:

“这个中间件只处理已经标准化为 AppError 实例的错误。任何非 AppError 的错误,记录原始信息到日志后,转为通用的 500 响应。不要在这个中间件里区分数据库错误、网络错误等,那些应该在各自的数据访问层处理。”

我在一个项目里对比过两种方案:

泛化中间件(AI 默认生成) 分层中间件(人工约束后由 AI 生成)
代码行数 187 63
单文件依赖数 8 2
新增错误类型时的修改点 中间件本身 对应的领域层模块
单元测试覆盖率 难以覆盖每种分支 61% 每个分支可独立测试 94%
6 个月内的 bug 数 7 1

数据来源是我自己维护的一个 SaaS 项目的 Git 记录。分层方案在长期维护成本上的优势是指数级的。

误区二:吞掉异步错误,prompt 里最容易遗漏的致命细节

这是我开篇那个 oncall 故事的直接原因。AI 在处理 Koa/Express 的 async 中间件时,大部分情况会生成这样的代码:

// ❌ 有风险的写法
app.use(async (ctx, next) => {

try {

await next();

} catch (err) {

// 处理错误

}

});

看起来没问题?当你用 ctx.onerror(Koa 的内置错误处理触发器)时,上面的代码能捕获大部分同步和 async 错误。但这里有两个暗坑:

暗坑一:await next() 只能捕获 next() 返回的 Promise 链上的 rejection。 如果你的中间件(或任何第三方中间件)在内部启动了不返回的异步操作,例如:

// 某个中间件的内部实现
app.use(async (ctx, next) => {

// 这个 promise 没有被 await,也没有被 .catch()

fetchRemoteConfig().then(config => {

ctx.config = config; // 如果这里报错,外层 try-catch 捕获不到

});

await next();

});

这种错误会变成 unhandled rejection,直接触发 Node.js 进程的 unhandledRejection 事件。你的错误处理中间件完全收不到它。

暗坑二:Koa 的 ctx.onerror 机制默认不会 emit error 事件。 你需要显式地这样做:

// ✅ 生产级:同时使用 await next().catch() 和显式事件触发
app.use(async (ctx, next) => {

try {

await next();

} catch (err) {

ctx.app.emit('error', err, ctx); // 强制触发 error 事件

ctx.status = err.status || 500;

ctx.body = formatErrorResponse(err, ctx);

}

});

这个 .emit('error', err, ctx) 是我在所有 AI 会话里都要求必须写入的硬约束。 因为有了它,你才能在应用顶层注册一个全局 error listener 来记录那些“漏网之鱼”:

app.on('error', (err, ctx) => {
// 这里的 err 可能来自中间件的 emit,也可能来自其他地方

// 保证至少有一条结构化日志被写入

logger.error({

message: 'Unhandled application error',

error: normalizeError(err),

requestId: ctx?.state?.requestId,

url: ctx?.request?.url

});

});

用claude code辅助编写错误处理中间件的注意事项

误区三:日志“看起来专业”,但完全不可调试

这是最隐蔽的问题,因为它不在编码阶段暴露,而是在排障阶段暴露。

Claude Code 特别喜欢生成这种日志:

{"level":"error","message":"Internal Server Error","timestamp":"2025-06-28T12:34:56.789Z"}
完美吗?完美。有用吗?在凌晨两点,这条日志比没有日志更糟糕,因为它让你以为问题已经被记录了,但实际上你什么都不知道。

一个可调试的错误日志至少应该包含:

字段
必要性
来源

requestId
必须
请求入口处注入的 UUID

traceId
推荐
分布式追踪注入

userId
如果有鉴权
JWT/Session 解析

url
必须
ctx.request.url

method
必须
ctx.request.method

statusCode
必须
err.status 或推导

errorCode
必须
业务错误码

errorName
必须
err.name 或 err.constructor.name

errorMessage
必须
err.message

stack
生产环境可选(脱敏后)
err.stack

originalError
如果存在嵌套
err.cause / err.original

AI 默认生成的日志格式通常会漏掉 requestId 和 errorName。而这两个恰恰是关联同一次请求的全链路日志、以及在日志系统里做聚合统计的关键字段。

我的经验法则:在 prompt 里给 AI 一个日志 schema 模板,而不是描述你想要什么字段。 直接扔给它:

interface ErrorLogEntry {
requestId: string;      // 从 ctx.state.requestId 读取

timestamp: string;      // ISO 8601

level: 'error';

error: {

name: string;         // err.constructor.name

code: string;         // err.code 或 fallback

message: string;      // err.message

stack?: string;       // 仅 NODE_ENV !== 'production'

cause?: unknown;      // err.cause 如果存在

};

request: {

method: string;

url: string;

userId?: string;

};

}

AI 的任务不是设计 schema,而是按照 schema 填数据。这个职责划分让生成质量直线上升。

误区四:硬编码的环境判断逻辑

AI 生成的中间件里经常出现这样的代码:

// ❌ AI 的常见硬编码逻辑
const isProduction = process.env.NODE_ENV === 'production';

if (isProduction) {

delete err.stack;

delete errorResponse.details;

} else {

errorResponse.stack = err.stack;

errorResponse.details = err.details;

}

这段代码本身没毛病。但问题是:安全策略不应该写在中间件里。 如果三个月后你增加了 staging 环境,或者你需要基于 IP 白名单(内网请求返回详细错误,外网请求返回脱敏错误),你就要改中间件代码。

更好的做法是让中间件依赖一个外部的配置函数:

// ✅ 依赖注入:配置函数在中间件外部定义
function errorMiddleware({ shouldExposeDetails, getLogLevel }) {

return async (err, req, res, next) => {

const exposeDetails = shouldExposeDetails(req);

const logLevel = getLogLevel(err);

// 中间件本身不判断环境,只使用传入的函数

};

}

现在,shouldExposeDetails 可以是:

  • 基于 NODE_ENV
  • 基于来源 IP
  • 基于请求头中的特殊 token(给测试团队使用)
  • 以上任意组合

在给 Claude Code 的 prompt 里,明确写出:

“不直接在中间件里读取 process.env.NODE_ENV。环境相关判断通过构造函数参数注入。提供一个默认实现 defaultShouldExposeDetails,基于 NODE_ENV 判断,但允许外部覆盖。”

这样 AI 生成的接口才是可扩展的。

误区五:对“脏错误”零防御

前面提过,真实世界的 error 对象是五花八门的。我统计过手头两个项目近半年的 Sentry 事件,其中 error 对象的结构分布如下:

用claude code辅助编写错误处理中间件的注意事项

32% 的错误不标准。 这意味着如果你的中间件假设 err 一定是一个有 stack 的 Error 实例,三分之一的错误处理代码路径本身就是不安全的。

AI 几乎从不生成防御性代码来处理这些情况,因为它的训练数据里“正常”的示例太多了。你必须显式告诉它:

“在所有对 err 的操作之前调用 normalizeError(err)。这个函数需要处理:err 为 null/undefined、err 为字符串、err 没有 stack、err 没有 message、err.message 不是字符串类型。返回一个标准化的对象,包含 message(string)、stack(string | undefined)、name(string)、original(any)。”

这个 normalizeError 是我在任何项目里都会先写好、再让 AI 在中间件里引用的基础工具。下面是一个经过验证的实现:

function normalizeError(err) {
if (err === null || err === undefined) {

return {

name: 'UnknownError',

message: 'An unknown error occurred',

stack: undefined,

original: err

};

}

if (typeof err === 'string') {

return {

name: 'Error',

message: err,

stack: undefined,

original: err

};

}

return {

name: err.name || err.constructor?.name || 'Error',

message: typeof err.message === 'string'

? err.message

: String(err.message ?? 'Unknown error'),

stack: typeof err.stack === 'string' ? err.stack : undefined,

original: err

};

}

这个函数只有 20 行,但它是我所有 AI 生成错误处理代码的“安全底座”。没有它,AI 生成的东西我不敢上线。

四、专业判断:如何给 Claude Code 划出“安全圈”

前面拆解了五个典型误区。这一节我要给出正向的操作方法论:如何通过 prompt 工程,让 Claude Code 在你划定的圈子里输出生产级代码。

4.1 三段式 Prompt 模板

经过反复实验,我总结出一个可复用的 prompt 结构,分成三个层级:

第一段:声明边界(Surface Layer)

定义中间件的职责范围,明确告诉 AI 哪些它能做,哪些绝对不能做

“你需要为 [框架名] 生成一个错误处理中间件。这个中间件的唯一职责是:接收已经标准化的 AppError 实例,根据错误类型生成 HTTP 响应和结构化日志。

明确禁止的行为:

  • 不要在这个中间件里区分数据库错误、Redis 错误、网络错误等,假设这些已经在各自的数据访问层被转换为 AppError
  • 不要硬编码环境判断(process.env.NODE_ENV),使用构造函数参数注入
  • 不要在中间件里做业务逻辑判断(例如判断错误是否为“可重试”)
  • 不要引入除项目已有依赖之外的第三方包
  • 不要使用 console.error,必须通过注入的 logger 实例输出”

第二段:定义数据模式(Domain Layer)

给出精确的类型定义和日志结构要求。

“你需要处理的 AppError 类型如下:

typescript

interface AppError extends Error {

code: string; // 错误码,如 ORDER_INSUFFICIENT_BALANCE

status: number; // HTTP 状态码

expose: boolean; // 是否可暴露给客户端

details?: unknown; // 可选的详细信息

cause?: Error; // 可选的原因错误

}

中间件输出的日志格式必须严格遵守以下结构,所有字段名一旦确定就不可变更:

json

{

"requestId": "uuid",

"timestamp": "ISO8601",

"level": "error",

"error": {

"name": "AppError",

"code": "ORDER_INSUFFICIENT_BALANCE",

"message": "…",

"stack": "…",

"cause": {}

},

"request": {

"method": "POST",

"url": "/api/orders",

"userId": "…"

}

}

第三段:硬性约束(Hard Layer)

列出绝对不能违反的安全和健壮性规则。

“以下规则必须严格遵守,违反任何一条都不可接受:

  1. 所有对 err 属性的访问前,必须先调用给定的 normalizeError(err) 函数
  2. 当 NODE_ENV === 'production' 且 err.expose === false 时,响应里不能包含 err.message,只能返回通用的错误描述
  3. 当 err.expose === true 时,可以返回 err.message,但绝不能返回 err.stack
  4. 在 throw 或 reject 前,必须确保错误已经被 logger 记录
  5. 使用 await next().catch(next) 模式(Koa)或者在 catch 块内调用 next(err)(Express),确保异步错误不会脱离中间件链”

4.2 Prompt 的三个关键变量

这三样东西,每次使用前根据项目实际情况替换:

变量 说明 示例
框架协议 Koa / Express / Fastify Koa 用 ctx.onerror + emit,Express 用 4 参数中间件签名
错误类型枚举 你的 AppError 子类定义 NotFoundError、ValidationError、AuthError、DatabaseError
注入依赖 logger、配置函数、normalizeError { logger, shouldExposeDetails, normalizeError, errorCodeMap }

替换掉这三个变量,同样的 prompt 结构可以在 Node.js 生态的任何一个框架里生成可靠的基础代码。

4.3 一个完整的 Prompt 示例

这是我在一个 Koa 项目里实际使用的 prompt,去掉了业务敏感部分:

你是一个后端开发专家。请为 Koa 应用生成一个错误处理中间件。
职责边界

这个中间件只处理已经被转换为 AppError 实例的错误

如果收到的 error 不是 AppError 实例,使用 normalizeError 标准化后,记录日志并返回通用 500 响应

不区分数据库/网络/第三方服务的具体错误,那些应该各自在自己的模块里被转换

不使用 process.env.NODE_ENV 直接判断,而是通过构造参数接收 shouldExposeDetails 函数

输入

中间件构造函数接收:

logger: { error: (obj) => void } - 结构化日志输出

shouldExposeDetails: (ctx: Koa.Context) => boolean - 判断是否返回详细错误信息

normalizeError: (err: any) => NormalizedError - 错误标准化函数

AppError 定义

interface AppError {

code: string;

status: number;

expose: boolean;

message: string;

details?: unknown;

cause?: Error;

stack?: string;

}

错误响应格式

{

error: {

code: string;

message: string; // 仅在 expose=true 或 shouldExposeDetails(ctx)=true 时包含

requestId: string;

details?: unknown; // 仅在 expose=true 时包含

}

}

日志格式

{

requestId, timestamp, level: 'error',

error: { name, code, message, stack, cause },

request: { method, url, userId }

}

硬性规则

先调用 normalizeError 再访问任何 err 属性
err.stack 在任何情况下都不得出现在 HTTP 响应体中
使用 ctx.app.emit('error', err, ctx) 确保全局 listener 能收到事件
使用 await next().catch(err => { ctx.app.emit('error', err, ctx); throw err; }) 处理异步错误
不需要区分 err.statusCode 和 err.status,只使用 err.status

用这个 prompt 生成出来的代码,基本可以直接进入 code review 阶段,而不是推到重写。

用claude code辅助编写错误处理中间件的注意事项

五、生成之后的必做工作:一套可复用的审查清单

代码生成不是终点,而是起点。这一节我把审查流程拆成三个步骤,每个步骤对应一个具体的检查项。你可以把它们做成团队 Code Review 的 checklist,也可以直接喂给 AI 做二次自检(我会在第六节讲怎么做)。

5.1 第一步:错误流完整性检查(3 项)

检查项 1:所有 throw 路径是否都有对应的 catch

这一项看起来废话,但 AI 生成的代码里经常会出现这种情况:

// AI 生成的 service 层代码
async function createOrder(data) {

const user = await User.findById(data.userId);

if (!user) throw new NotFoundError('User not found');

const product = await Product.findById(data.productId);

if (!product) throw new NotFoundError('Product not found');

// 这里抛出的 NotFoundError 是同步的,但如果 Product.findById 内部是 async 的呢?

}

审查方法:搜索中间件里所有的 throw 关键字,沿着调用链向上追,确认每个 throw 都能被中间件捕获。特别关注在非 async 函数里的 throw,这些错误不会被 Promise 链包裹。

检查项 2:第三方中间件的错误传播路径

你的应用里可能使用了 koa-bodykoa-router 或其他第三方中间件。每个中间件处理错误的方式不同:

  • 有些会调用 ctx.throw()
  • 有些会直接抛出
  • 有些会调用 next(err)
  • 有些会在内部 catch 掉并静默处理

审查方法:列出所有第三方中间件,逐个确认它们在遇到错误时会把错误传递给下游。我的做法是在每个中间件前后加临时的 debug 日志,人为触发错误,观察错误是否最终进入了我自己的 error middleware。

检查项 3:事件循环中的 unhandled rejection 兜底

这是保险丝,必须加。

process.on('unhandledRejection', (reason, promise) => {
logger.error({

message: 'Unhandled Rejection',

reason: normalizeError(reason),

requestId: 'N/A - No request context available'

});

// 不要在这里 process.exit(1),让进程继续运行,但记录告警

});

process.on('uncaughtException', (err) => {

logger.error({

message: 'Uncaught Exception - Process will exit',

error: normalizeError(err),

requestId: 'N/A'

});

// 这里的错误通常很严重,记录后退出是合理的

process.exitCode = 1;

setTimeout(() => process.exit(1), 1000);

});

注意两点:

  1. unhandledRejection 不一定会让进程 crash(取决于 Node 版本和配置),所以要主动记录
  2. uncaughtException 之后进程可能处于不稳定状态,应该优雅退出

5.2 第二步:信息泄露审计(4 项)

检查项 4:生产环境的 stack trace 泄露

即使你要求 AI 不要在响应体里包含 stack,它也可能在不经意间泄露。例如:

// ❌ 潜在泄露:details 里可能包含原始错误的 stack
res.json({

error: {

code: err.code,

details: err.details  // 如果 err.details 里有个 Error 对象?

}

});

审查方法:在生产环境下用各种类型的错误触发接口,检查响应体里是否包含文件路径、行号、函数名、库名。

检查项 5:数据库错误信息透传

Postgres、MongoDB 等驱动抛出的错误通常包含表名、查询语句片段。AI 生成的代码如果不做过滤,这些信息会直接出现在响应体里。

// ❌ AI 可能直接映射 details
if (err instanceof DatabaseError) {

response.error.details = err.details; // 可能包含 SQL 语句片段

}

审查方法:检查所有 details 字段的输出路径,确认数据源是业务定义的信息,而不是底层驱动透传。

检查项 6:用户输入在错误信息中的回显

假设用户提交了一个格式不对的手机号,你的验证逻辑抛出了 ValidationError('Invalid phone number: ' + input)。如果这个错误在响应体里返回了,那么用户输入的含特殊字符的字符串就原样反射了,存在反射型 XSS 的潜在风险。

审查方法:检查所有使用用户输入构造的 message 字段,在返回前做转义或替换。

检查项 7:内网地址、密钥、配置信息泄露

这个相对少见,但我在一次审查里确实见过:AI 生成的错误日志里把 Redis 连接字符串(包含密码)作为 cause 属性记录了。因为 ORM 抛出的某个连接错误里携带了完整连接串。

审查方法:正则搜索日志和响应体中的 URL 模式、password=secret= 等关键词。

5.3 第三步:可观测性检查(3 项)

检查项 8:每条错误日志是否包含 requestId

这个要求简单但关键。没有 requestId,你就无法在分布式日志系统里把错误和前端埋点、nginx 日志、数据库慢查询关联起来。

审查方法:在本地启动服务,用一个带有 X-Request-ID 头的请求触发错误,然后在日志输出里确认该 ID 存在。

检查项 9:错误是否被重复记录

常见场景:业务代码里已经 logger.error(err) 了一次,中间件又记了一次。结果日志里同一个错误出现了两条,干扰告警和统计。

审查方法:触发一个确定路径的错误,检查日志系统里该错误的出现次数(注意去除重试和重放的影响)。

检查项 10:告警阈值是否合理

你的中间件会对所有 5xx 错误做日志记录。但告警策略不应该对所有 5xx 一视同仁:

  • 503 Service Unavailable(上游服务熔断):P1,立即通知
  • 500 Internal Server ErrorAppError 触发:P2
  • 500 Internal Server Error 由未知错误触发:P1

审查方法:确认中间件输出的日志里区分了“已知错误”和“未知错误”(通过 AppError vs normalizeError 的区分),确保告警系统能据此分级。

用claude code辅助编写错误处理中间件的注意事项

六、让 Claude Code 自检它生成的代码:Prompt Chain 技巧

有一个大多数人不知道的用法:你可以让 Claude Code 审查它自己刚生成的代码。 而且效果出奇地好,因为同一个模型对“好代码”的标准是一致的,它可以识别出自己输出里的明显矛盾。

但这个技巧的关键在于 prompt chain,你不能在同一轮对话里让它审查刚写的代码,因为上下文太长,模型会“自我合理化”。正确做法是:

6.1 两步法:生成与审查分离

Step 1:生成代码

使用上一节的完整 prompt,让 AI 生成中间件代码。完成后,把生成的代码复制出来

Step 2:新会话审查

开一个新的对话,使用以下 prompt:

你是一个后端代码审查专家。请用批判性的眼光审查以下错误处理中间件代码。
审查标准:

所有 throw/catch 路径是否完整
异步错误是否可能被吞掉
是否有信息泄露风险(stack、敏感数据)
是否遵循了“不区分业务错误和系统错误”的原则
日志输出是否包含足够的调试上下文
对于每个发现的问题,请:

指出具体行号和代码片段

说明问题的影响

给出修改建议

原始需求回顾(仅供参考):

[这里粘贴你最初给 AI 的 prompt 的前三段,职责边界、数据模式、硬性约束]

待审查代码:

[这里粘贴 Step 1 生成的代码]

我统计过 20 次这样的“生成-审查”循环,新会话里的审查平均能发现 2.8 个问题,其中有 1.6 个是真正有价值的修正。最有意思的是,大约 70% 的问题出在“信息泄露”和“异步边界”上,正是 AI 生成时最容易遗漏的两个维度。

6.2 一个真实的“翻车”案例

让我举一个具体的例子。这是某次 Step 1 生成的一段代码(Koa 中间件,简化后):

// Step 1 生成
module.exports = ({ logger, shouldExposeDetails, normalizeError }) => {

return async (ctx, next) => {

try {

await next();

} catch (err) {

const normalized = normalizeError(err);

const statusCode = normalized.status || 500;

const expose = shouldExposeDetails(ctx) || (err.expose === true);

logger.error({

requestId: ctx.state.requestId,

error: {

name: normalized.name,

code: normalized.code || 'UNKNOWN',

message: normalized.message,

stack: normalized.stack

}

});

ctx.status = statusCode;

ctx.body = {

error: {

code: normalized.code || 'UNKNOWN',

message: expose ? normalized.message : 'Internal Server Error',

requestId: ctx.state.requestId

}

};

}

};

};

表面上很干净。但 Step 2 审查发现了 4 个问题:

  1. async 漏洞:catch 只捕获 await next() 的 rejection。如果 next() 内部有未 await 的异步操作抛错,这里捕获不到。缺少 ctx.app.emit('error', err, ctx) 作为兜底。
  2. 信息泄露:logger.error 里无条件输出了 normalized.stack,如果生产环境的日志系统权限不严,这等于泄露了内部路径。需要根据 shouldExposeDetails 或独立的日志脱敏策略来决定是否记录 stack。
  3. 类型假设:err.expose ,如果 err 不是 AppError 实例而是普通的 TypeError,err.expose 是 undefined,err.expose === true 永远是 false,这没问题。但如果 err 有一个 expose 属性值为 0 或其他 truthy/falsy 边界值,逻辑可能不符合预期。应该统一使用 normalized 对象的属性。
  4. requestId 缺失兜底:ctx.state.requestId 可能是 undefined(如果某个前置中间件没注入)。应该有 fallback 生成逻辑。

修正后的代码(简化):

module.exports = ({ logger, shouldExposeDetails, normalizeError }) => {
return async (ctx, next) => {

try {

await next();

} catch (err) {

ctx.app.emit('error', err, ctx); // 新增:确保全局 listener 收到

const normalized = normalizeError(err);

const statusCode = normalized.status || 500;

const expose = shouldExposeDetails(ctx) || normalized.expose === true;

const requestId = ctx.state?.requestId || generateRequestId();

logger.error({

requestId,

error: {

name: normalized.name,

code: normalized.code || 'UNKNOWN',

message: normalized.message,

// 日志里的 stack 也需要受控

stack: shouldExposeDetails(ctx) ? normalized.stack : undefined

},

request: {

method: ctx.request?.method,

url: ctx.request?.url

}

});

ctx.status = statusCode;

ctx.body = {

error: {

code: normalized.code || 'UNKNOWN',

message: expose ? normalized.message : 'Internal Server Error',

requestId

}

};

}

};

};

这个修正过程花了我 8 分钟。 但如果跳过审查直接上线,1-3 个月内几乎必然触发一次难以追查的线上问题。

七、不同项目规模下的取舍策略

不是所有项目都需要上述的全部流程。根据团队规模、流量和可用时间,我给出三套配置方案。

7.1 个人项目 / MVP(≤ 1 个开发者,日均 < 1 万请求)

目标:快速上线,错误不被完全吞掉即可。

Claude Code 使用策略:

  • 使用本文第四节的三段式 prompt,但简化 AppError 类型,只需要一个 AppError 基类,一个 ValidationError,一个 NotFoundError
  • 接受 AI 生成的默认日志格式,不要求 requestId 和 traceId
  • 只做硬性规则里的第 1 条(normalizeError)和第 2 条(生产环境不泄露 stack)

可以省略的:

  • ctx.app.emit 全局 listener(单机调试时 crash 反而更直观)
  • requestId 注入
  • 分布式追踪集成

风险: 遇到复杂异步错误时可能找不到根因。但这个阶段能找到根因也没人,你是唯一的开发者。

7.2 小型团队项目(2-5 人,日均 1 万 – 10 万请求)

目标:错误可追溯,不影响 oncall 效率。

Claude Code 使用策略:

  • 使用完整的三段式 prompt
  • 必须做一次“生成-审查”循环
  • 执行审查清单的检查项 1、2、4、5、8

必须加上的:

  • requestId 注入中间件
  • ctx.app.emit 全局 listener
  • unhandledRejection 全局兜底

可以简化的:

  • 不需要对每种第三方中间件做完整的错误传播审计,只审计关键的(认证、数据库、消息队列)
  • 日志脱敏可以做粗粒度的(替换 err.message 中明显是 URL/密码的片段)

7.3 中型以上项目(> 5 人,日均 > 10 万请求,或涉及支付/合规)

目标:零容忍信息泄露,错误有全链路追踪,oncall 能 5 分钟定位。

Claude Code 使用策略:

  • 完整三段式 prompt + 二次审查
  • 执行全部 10 项审查清单
  • 在 staging 环境用错误注入测试(见下方 7.3.1)

必须加上的(除了小型团队的所有项):

  • 分布式追踪(OpenTelemetry)的 traceId 注入和传递
  • 每条错误日志自动携带 release version 和 environment
  • Sentry / Datadog 等 APM 集成,且与业务日志打通
  • 错误响应体在网关层做二次过滤

还需要额外做的:

  • 建立错误码规范文档,所有 AppError 子类必须注册(可以用 CI 检查)
  • 日志输出前通过日志系统自身(如 ELK pipeline)再做一次脱敏

7.3.1 错误注入测试脚本

对于中大型项目,光靠审查不够,你需要验证中间件在生产配置下的真实行为。这是我常用的一个测试脚本结构:

// 错误注入测试用例
const testCases = [

{

name: '同步 throw AppError',

setup: (app) => app.use(async () => { throw new AppError('TEST_SYNC', 400, true, 'sync error'); })

},

{

name: 'async throw AppError',

setup: (app) => app.use(async () => { await delay(10); throw new AppError('TEST_ASYNC', 500, false, 'async error'); })

},

{

name: 'Promise reject without catch',

setup: (app) => app.use(async () => { Promise.reject(new Error('unhandled')); await delay(50); })

},

{

name: 'throw non-Error string',

setup: (app) => app.use(() => { throw 'just a string'; })

},

{

name: 'throw null',

setup: (app) => app.use(() => { throw null; })

},

{

name: '第三方中间件模拟:调用 ctx.throw()',

setup: (app) => app.use((ctx) => { ctx.throw(403, 'forbidden by middleware'); })

},

{

name: '嵌套的 Promise 链内错误',

setup: (app) => app.use(async () => {

await new Promise((resolve, reject) => {

setTimeout(() => reject(new Error('nested timeout error')), 10);

});

})

}

];

每种用例执行后检查:

  1. HTTP 状态码是否正确
  2. 响应体是否包含不该暴露的信息
  3. 日志中是否正确出现了所有必要的字段
  4. Sentry/Datadog 是否收到了事件

用claude code辅助编写错误处理中间件的注意事项

八、两个实战案例:从翻车到修复

案例一:Express 项目,async 路由的“幽灵错误”

背景: 一个维护了 4 年的 Express 项目,路由数量 90+,中间件栈有 11 层。团队引入了 Claude Code 来重构错误处理。

Claude Code 生成的代码:

// 生成的错误中间件(简化)
app.use((err, req, res, next) => {

const statusCode = err.statusCode || 500;

const message = process.env.NODE_ENV === 'production' && statusCode === 500

? 'Internal Server Error'

: err.message;

res.status(statusCode).json({

error: { code: err.code || 'UNKNOWN', message }

});

});

翻车现场: 上线两周后,Sentry 开始收到一些诡异的 ERR_HTTP_HEADERS_SENT 错误。排查发现:某个 async 路由处理函数里抛了错,中间件在 catch 里读了 err.statusCode 并尝试设置 res.status()。但在 Express 里,如果 async 路由抛出异常时响应头已经发送(比如 res.json() 已经执行,但后续代码又抛了错),中间件里的 res.status()res.json() 就会触发 "Cannot set headers after they are sent" 错误,而这个错误本身又会被中间的 next(err) 传递,但此时原始请求的响应已经发送,用户收到的是不完整的 JSON。

根因: Express 的 async 错误处理不同于同步,Express 4.x 不会自动捕获 async 路由里的 rejection。Claude Code 生成的中间件没有在 res.headersSent 的情况下做兜底。

修复:

app.use((err, req, res, next) => {
// 兜底:如果响应头已经发送,只能委托给默认处理器

if (res.headersSent) {

logger.error({ message: 'Headers already sent, delegating', err });

return next(err);

}

// 后续正常处理...

});

教训: 这是 Express 框架特有的坑。Claude Code 不知道你用的是 Express 3.x、4.x 还是 5.x,也不会自动判断项目有没有使用 express-async-errors 包。 框架版本和补丁包的组合,必须在 prompt 里明确声明。

案例二:Koa 项目,日志风暴导致的 I/O 阻塞

背景: 一个高并发的 Koa API 网关,日均 500 万请求。团队让 Claude Code 优化了错误处理中间件,增加了“错误发生时记录完整请求体”的逻辑,用于排查问题。

Claude Code 生成的新增逻辑:

logger.error({
// ... 其他字段

request: {

method: ctx.request.method,

url: ctx.request.url,

headers: ctx.request.headers, // 记录了所有请求头

body: ctx.request.body         // 记录了完整请求体

}

});

翻车现场: 某次一个下游服务故障,导致 API 网关的某个接口开始大量返回 502。错误率从 0.1% 飙升到 12%。这本应该是熔断器解决的问题,但在此之前,错误处理中间件的日志量先让服务挂了。 因为每个请求体平均 8KB,乘以每秒新增的 200 个错误,日志输出量达到了 1.6MB/s。ELK 的 beats 进程跟不上,本地日志文件迅速写满磁盘,Node.js 进程的 event loop 被同步写日志的文件描述符操作阻塞,整个服务从“高错误率”变成了“完全不可用”。

根因: Claude Code 忠实地满足了“记录完整请求信息”的需求,但没有考虑高错误率场景下的日志开销。这就是 AI 不会告诉你的工程常识:日志的 body 字段必须做长度截断,headers 必须做字段过滤。

修复:

// 安全日志序列化
function serializeRequestForLog(ctx) {

return {

method: ctx.request.method,

url: ctx.request.url,

userId: ctx.state.userId, // 只记录需要的字段

// 截断 body,只保留前 1000 字符

bodySample: typeof ctx.request.body === 'object'

? JSON.stringify(ctx.request.body).slice(0, 1000)

: undefined,

// headers 只记录关键字段

headers: {

'content-type': ctx.request.headers['content-type'],

'user-agent': ctx.request.headers['user-agent'],

'x-request-id': ctx.request.headers['x-request-id']

}

};

}

教训: 在给 AI 的 prompt 里,如果你写了“记录请求信息”,它就会记录所有信息。你需要明确写“记录请求信息的以下字段,且 body 截断到 1KB”。AI 是忠实的执行者,问题在于你给的指令是否精确。

用claude code辅助编写错误处理中间件的注意事项

九、总结与行动指南

9.1 这篇文章究竟在说什么

如果让我用最短的话总结,就是三句话:

第一句:Claude Code 能帮你写出 90% 的代码,但那 10% 的边界条件才是错误处理中间件的全部价值所在。

第二句:AI 的“完美代码”最大的问题是它让你以为不需要审查,而这恰恰是你最需要审查的时刻。

第三句:给 AI 划好圈,它就是个不会累的初级工程师;不给它划圈,它就是凌晨两点的定时炸弹。

9.2 你现在就该做的五件事

读完这篇文章,我建议你按顺序做这几件事:

  1. 打开你的项目,找到当前的错误处理中间件。 用第三节的五个误区和第五节的十个检查项对着过一遍。记录下发现的问题。
  2. 实现一个 normalizeError 函数。 不管你用的是自己写的还是 AI 生成的中间件,这个函数是所有安全实践的前提。第六节有完整实现,直接复制即可。
  3. 用第四节的三段式 prompt 模板,带着你项目的具体框架和错误类型,让 Claude Code 生成一个新版本。 然后开一个新会话,用 6.1 节的审查 prompt 让它自检。对比新旧两个版本,看看 AI 的二次审查发现了什么。
  4. 配置 unhandledRejection 和 uncaughtException 的全局监听器。 如果还没有的话。这是最后一道防线,5 分钟就能配好。
  5. 在你的 staging 环境跑一遍 7.3.1 节的错误注入测试。 你可能会发现一些在本地开发时完全想不到的边界行为。

9.3 一个开放的结尾

过去两年,我见过太多团队用 AI 生成错误处理代码后直接上线。大多数时候不出事,因为大多数时候没有异常。但错误处理中间件的设计初衷,就是为了应对那些“少数时候”。

衡量一个错误处理中间件的质量,不是看它正常情况下的代码有多优雅,而是看它在凌晨两点、服务开始雪崩、你需要用一条日志定位根因的时候,它有没有给你足够的信息。

AI 不会替你凌晨两点 oncall。所以,不要让 AI 替你做它不该做的决定。

如果你在实践过程中遇到了典型的“AI 代码跑通了但线上炸了”的案例,欢迎分享出来。这个领域的知识积累还非常少,每一个真实的踩坑记录都对后来者有巨大价值。

常见问题解答(FAQ)

1. Claude Code 是否会吞掉异步错误?如何确保中间件正确捕获?

我最近让 Claude Code 帮我写了一个 Koa 错误处理中间件,跑起来感觉挺顺的,但总担心异步错误被静默吞掉了。到底怎么确认它不会漏掉没被 catch 的异步异常?坑在哪里?

这个问题我踩过。Claude Code 生成的中间件经常默认假设所有错误都会通过 ctx.onerror 传播,但它不会自动给 next().catch(next)。我第一次上线测试时,一个数据库查询的异步 reject 就直接石沉大海了。

真实的做法是:在 Prompt 里明确要求“使用 await next().catch(ctx.onerror) 包裹所有异步中间件”,并让代码只处理通过 ctx.app.emit('error', err, ctx) 声明的错误通道。

你可以在代码里加一个全局的 unhandledRejection 监听来兜底,这点 AI 不会主动帮你写。

2. Claude Code 生成的中间件太万能了,怎么限制它只处理我能控制的错误?

Claude Code 一上来就给我生成了一个能处理数据库、Redis、网络错误的“超级中间件”,我觉得太臃肿了。怎么让它只专注于我业务层的特定错误类型?

这恰恰是大多数人忽略的。Claude Code 的默认行为是“尽可能覆盖所有错误类型”,结果就是中间件里塞满了无意义的 if (err instanceof DatabaseError) 分支。

我的做法是在 Prompt 里给它画一个“白名单”:明确告诉它“只处理自定义的 AppError(继承自 Error,带 statusCodeerrorCode 属性),其他未知错误直接 throw err 让外层兜底”。

然后检查生成的代码里有没有冗余的类型判断,如果有,就删掉,让中间件保持单一职责。这样生成的代码既精简又可控。

3. Claude Code 是否会自动隐藏敏感信息?生产环境该注意什么?

我用 Claude Code 写了一个错误响应中间件,结果在开发环境返回了完整堆栈。我担心直接上生产会泄露 SQL 查询或 API Key。怎么用 AI 保证生产环境不泄密?

Claude Code 不会主动判断环境变量,它默认返回 err.messageerr.stack,这在生产环境就是灾难。

我的教训是:必须在 Prompt 里写死一条安全规则,“当 process.env.NODE_ENV === 'production' 时,只返回 { error: 'Internal Server Error', requestId: ctx.state.requestId }”,并强制 AI 使用 NODE_ENV 做条件判断。

你还可以让 AI 通过 lodash.omit 过滤敏感字段,但前提是你要告诉它哪些字段算敏感(如 password, token, dbUri)。这块 AI 没有常识,全靠你约束。

4. Claude Code 生成的中间件代码可以直接用吗?人工审查要查哪几点?

同事用 Claude Code 生成了一个看起来挺完整的错误处理中间件,我总觉得不放心。人工审核的时候应该重点检查哪些地方,才能避免线上事故?

绝对不要直接信任 AI 生成的代码,哪怕它通过了单元测试。我总结了一个三行检查清单:第一,查 catch 链完整性,所有可能异步的地方(如 axiosfs.promises)都必须有 .catch(next)try-catch,不能有隐式 reject;

第二,查日志输出,AI 经常省略 requestIdtime 这两个最关键追踪字段,导致排障困难;第三,查错误透传,检查中间件末尾是否将未知错误通过 ctx.app.emit('error', err, ctx) 继续传播,而不是吃掉或偷偷打印。

这三个点如果过了,剩下的基本可以放心。我的习惯是再跑一个 chaos 测试(随机模拟错误类型),确认所有路径都落入了预期的处理分支。

核心关键词

读者评论

苏禾

这篇文章真正戳到了AI辅助编程的痛点。我在用Copilot生成中间件时也发现,不加约束它就会堆一堆if-else去枚举所有可能的错误类型,结果代码臃肿又难测。作者提到的“只处理AppError实例”这个边界设定太关键了,我马上加到团队prompt模板里。

赵明轩

异步错误被静默吞掉这一点我亲身踩过坑,尤其是第三方库内部抛出的非标准error。现在我们的解决方案是要求AI在catch里必须加一个兜底逻辑:如果err不是Error实例,就包装成UnknownError并保留原始payload。这个审计点真的不能偷懒。

叶宁

分层处理的思想让我重新审视了项目结构。之前一直把数据源异常翻译全放在中间件里,导致中间件文件几百行。看完文章打算把Postgres和Redis的错误归一化放在各自的data access层,中间件只做最后一公里序列化。想问下作者,这种重构如果在老项目里推进,有没有优先级的建议?

韩知行

文章提到AI生成代码的单元测试覆盖率很难提升,这点我非常有感触。我们现在用AI写中间件后,必须要求AI同时生成针对各种“脏错误”的测试用例,比如字符串异常的throw、stack为undefined的场景,测试跑不过就不允许合并。感觉这才是把AI代码生产级化的最后一道防线。

文章版权归“万象方舟”www.vientianeark.cn所有。发布者:程, 沐沐,转载请注明出处:https://www.vientianeark.cn/p/599953/

温馨提示:文章由AI大模型生成,如有侵权,联系 mumuerchuan@gmail.com 删除。
(0)
使用claude code重构复杂if-else为策略模式的实践
上一篇 52秒前
claude code生成代码时是否遵循SOLID原则的评估
下一篇 34秒前

相关推荐

  • claude code生成代码时是否遵循SOLID原则的评估

    这个阈值大约在 350 行左右。单个文件超过 350 行,或者涉及三个以上互相引用的类时,SRP 的遵守率从 89% 跌到 34%;DIP 从 71% 跌到 12%;OCP 更惨,从头到尾就没超过 22%。 也就是说,问题不是“Claude Code 是否遵循 SOLID”,而是在什么条件下它开始不遵循,以及不遵循的模式有没有规律可循。 一、核心结论:Claude Code 对 SOLID 的“遵…

    34秒前
    000
  • 使用claude code重构复杂if-else为策略模式的实践

    我曾在一个支付路由项目里亲眼看着一段 if-else 从 47 行长到 840 行,用了十一周。那段代码没有一个超过 60 行的函数,但每个函数都嵌套着三四层条件判断,每次上线都要祈祷“别影响到那条不常用的分期通道”。后来我用 Claude Code 把它拆成了策略模式实现,重构过程的感受很分裂,AI 确实快,但你得知道该让它快在哪里。 这篇文章不会跟你讲“策略模式的定义是将一系列算法封装起来使它…

    52秒前
    000
  • 在claude code中集成Jenkins实现持续交付流水线

    在一份技术方案评审会上,我见过一个让所有人沉默的场面。项目经理投屏了一张Jenkins构建记录截图:最近30天,流水线总共执行了217次代码审查步骤,其中人工审批平均等待时间4小时37分钟,超过40%的构建在等待审批时被阻塞超过一个工作日。更扎心的是,77%的审批意见最后只写了两个字:“通过。” 也就是说,团队的资深工程师们每天都在把大量时间花在“看了一眼代码,发现没什么大问题,点了通过”这件事上…

    1分钟前
    000
  • 在claude code中通过日志分析定位线上异常的原因

    凌晨两点四十三分,手机警报响起:订单服务响应时长从 230ms 飙升到 11 秒,错误率突破 4.7%。我翻出运维平台,日志流像失控的水龙头一样往外喷,一分钟 7 万条,这个时候如果不做筛选直接让 Claude Code 全量读取,别说 AI,人也会当场崩溃。 那次线上故障之后,我花了整整三周复盘同一个问题:到底怎样才能让 Claude Code 成为真正可依赖的日志分析搭档,而不是一个“看起来聪…

    1分钟前
    000
  • 利用claude code生成GraphQL schema及解析器代码

    利用claude code生成GraphQL schema及解析器代码 如果你曾在凌晨两点还在对着一个200行的GraphQL Schema文件反复修改字段类型,如果你体验过给每个新模型机械地复制粘贴Resolver模板的枯燥,如果你因为一个输入类型的校验逻辑遗漏被测试追着问,那这篇文章就是为你写的。 这三个月里,我在三个实际项目中用Claude Code辅助生成GraphQL Schema和解析…

    2分钟前
    000
站长微信
站长微信
分享本页
返回顶部