新人第一次让 AI 帮忙补异常处理时,往往会觉得这件事非常省心。
你把一段报错代码贴进去,告诉它:
这个接口偶尔会抛异常,帮我补一个不会让页面崩掉的处理方式。
很快,它可能会给你补上 try/catch、日志、默认值、失败提示,甚至顺手把调用方也改得"更稳一点"。
本地跑一下,接口不再直接报 500。 页面也不再出现空白。 测试里正常请求和异常请求都能返回对象。
看起来问题解决了。
但异常处理最容易出现的坑,不是"没有 catch"。
而是为了让系统看起来正常,把一个本来应该被看见的失败,悄悄伪装成了成功。
这类问题刚出现时通常不会特别明显。
用户看到的是"任务已提交"。 开发者看到的是 HTTP 200。 前端看到的是一段格式正常的数据。 而真正的任务可能根本没有创建,队列没有收到消息,数据库没有写入成功,后续流程也不可能继续。
接口没有报错。
但系统已经在悄悄说谎。
一个很常见的场景:导出任务显示已创建,实际上根本不存在
假设你们有一个"导出数据"接口。
用户点击导出后,后端创建一条任务记录,再把任务投递到队列中,由后台 Worker 异步生成文件。
一个简化版本可能是这样:
ts
async function createExportTask(input: ExportInput) {
const task = await exportTaskRepository.create({
userId: input.userId,
fileType: input.fileType,
status: "queued",
});
await exportQueue.add("generate-export", {
taskId: task.id,
});
return {
taskId: task.id,
status: "queued",
};
}
这段逻辑里至少有两个可能失败的位置:
- 创建任务记录失败;
- 任务记录成功创建,但投递队列失败。
新人让 AI "补个异常处理",很容易拿到类似这种代码:
ts
async function createExportTask(input: ExportInput) {
try {
const task = await exportTaskRepository.create({
userId: input.userId,
fileType: input.fileType,
status: "queued",
});
await exportQueue.add("generate-export", {
taskId: task.id,
});
return {
success: true,
taskId: task.id,
status: "queued",
};
} catch (error) {
logger.warn("create export task failed", error);
return {
success: true,
taskId: "pending",
status: "queued",
};
}
}
这段代码乍看很"用户友好"。
它避免了接口直接失败。 调用方始终能拿到格式一致的结果。 前端也不用额外处理异常分支。
但它真正制造的是一种更难排查的问题:
text
用户点击导出
↓
后端创建任务失败
↓
接口仍然返回 success: true
↓
前端显示"导出任务已创建"
↓
用户开始等待一个永远不会完成的任务
↓
开发者只能从后续投诉里发现异常
这不是错误处理。
这是把错误从一个明确的地方,移动到了更晚、更隐蔽、更难定位的地方。
"不抛异常"不等于"请求成功"
很多新人会把接口结果简单分成两类:
text
有报错 = 失败
没报错 = 成功
但真实系统里,至少应该拆成三类。
| 结果类型 | 用户看到什么 | 系统应该做什么 | 是否可以返回成功 |
|---|---|---|---|
| 已完成 | 操作确实完成 | 返回结果与资源标识 | 可以 |
| 已接收但未完成 | 任务已被可靠接收 | 返回真实任务 ID 和可查询状态 | 可以,但必须明确是异步处理中 |
| 未完成 | 没有创建成功、没有写入成功、没有投递成功 | 返回可定位错误与恢复建议 | 不应该伪装为成功 |
关键点在于:
"接口成功返回"不一定代表业务完成, 但"业务没有被可靠接收"时,也不能把它包装成成功。
例如,任务已经成功写入数据库,但队列暂时不可用。
这时可以考虑返回:
json
{
"success": false,
"code": "EXPORT_QUEUE_UNAVAILABLE",
"message": "导出任务暂未进入处理队列,请稍后重试",
"requestId": "req_xxx",
"retryable": true
}
也可以在具备可靠补偿机制的前提下,返回"已接收,待调度"。
但前提是:系统里真的存在一条可追踪、可恢复的任务记录。
不能因为"前端希望拿到成功状态",就凭空生成一个不存在的 taskId。
最危险的不是 catch,而是 catch 之后返回了什么
异常处理里,catch 本身通常不是问题。
真正的问题是:捕获异常以后,你让调用方相信了什么。
我建议新人把异常处理分成四个问题来看。
1. 这个操作到底有没有完成
例如:
text
数据库写入成功了吗?
队列任务真的投递了吗?
文件真的上传了吗?
状态真的更新了吗?
外部服务是否已确认收到请求?
如果答案不确定,就不要返回"已完成"。
2. 失败是不是可以安全重试
不是所有异常都适合直接重试。
例如:
text
网络请求超时
↓
你不知道对方是否已经收到请求
↓
如果再次发起调用
↓
可能导致重复创建、重复扣减、重复发送
这时候必须考虑幂等键、任务状态和重复请求。
一个"再试一次"的建议,只有在你知道重复执行不会造成副作用时才安全。
3. 调用方能不能知道下一步该做什么
好的错误返回不只是:
json
{
"message": "系统异常"
}
而应该尽量告诉调用方:
- 这次请求是否已经被接收;
- 是否可以重试;
- 是否需要等待;
- 是否需要人工处理;
- 是否有可追踪的请求标识。
例如:
json
{
"success": false,
"code": "EXPORT_TASK_CREATE_FAILED",
"message": "导出任务创建失败,请稍后重试",
"requestId": "req_20260624_xxx",
"retryable": true
}
这比返回一个模糊的"操作失败"更有价值。
4. 失败后,团队还能不能定位问题
异常处理如果只做了一件事:
ts
catch (error) {
return { success: false };
}
那它虽然没有假装成功,但依然不够好。
因为下一步会出现:
- 用户说"导出不了";
- 前端说"后端返回失败";
- 后端说"没有更多信息";
- 日志里找不到是哪一次请求;
- 监控里也不知道失败率有没有升高。
所以错误返回至少要有一条可关联的线索。
通常可以是:
text
requestId
taskId
traceId
errorCode
operationId
它不需要把内部异常栈暴露给用户。
但必须让开发团队能从一个有限信息,找到真正的失败位置。
先让 AI 设计错误契约,再让它补代码
很多人给 AI 的指令是:
text
给这个方法补 try/catch,避免接口报错。
这类指令很容易把目标带偏。
因为它把"避免报错"当成了第一目标。
更稳的写法应该是先让 AI 设计错误语义。
text
请不要直接修改代码。
当前方法用于创建异步导出任务,步骤包括:
1. 创建任务记录;
2. 投递消息队列;
3. 返回任务状态给调用方。
请先分析:
1. 每一步可能失败的情况;
2. 哪些失败代表任务未创建;
3. 哪些失败可以安全重试;
4. 哪些失败必须保留错误状态,不能返回成功;
5. 调用方需要拿到哪些字段才能继续处理;
6. 需要记录哪些日志和监控指标;
7. 哪些异常必须要求人工确认。
最后再给出:
- 错误码设计;
- 返回结构;
- 测试场景;
- 候选代码。
这一步看起来多花了一点时间。
但它会让 AI 从"帮你补一个 catch"变成"帮你设计一次失败怎么被系统承认"。
两者差别非常大。
前者关注的是代码不要抛错。 后者关注的是失败以后,系统还能不能保持可信。
一个可复用的错误返回契约
对于普通服务接口,可以先从一个简单、统一的错误结构开始。
ts
type ApiResult<T> =
| {
success: true;
data: T;
requestId: string;
}
| {
success: false;
code: string;
message: string;
requestId: string;
retryable: boolean;
};
当任务是异步类型时,还应进一步区分:
ts
type AsyncTaskResult =
| {
success: true;
accepted: true;
taskId: string;
status: "queued" | "processing";
requestId: string;
}
| {
success: false;
accepted: false;
code: string;
message: string;
requestId: string;
retryable: boolean;
};
这里最重要的不是类型写法。
而是要明确:
text
success: true
必须代表任务真的被系统可靠接收。
accepted: true
必须代表存在一个可查询、可恢复、可追踪的任务记录。
retryable: true
必须代表重复请求不会造成不可控副作用,或者系统已经有幂等机制。
不要把这些字段当成"前端展示用的文案"。
它们其实是服务之间的行为约定。
新人最容易漏掉的四种失败路径
AI 在补异常代码时,通常会优先照顾最容易想到的异常。
例如网络错误、空指针、数据库异常。
但工程里更需要主动设计的,往往是下面这些路径。
1. 写入成功,后续动作失败
text
任务记录已创建
↓
队列投递失败
↓
用户以为任务已开始
↓
实际上没有 Worker 会处理它
这类问题需要考虑:
- 事务;
- outbox;
- 延迟重试;
- 定时补偿;
- 明确失败状态。
不能只靠一个 catch 把错误吞掉。
2. 调用超时,但对方可能已经执行
text
请求外部服务
↓
本地等待超时
↓
不知道对方有没有收到
↓
再次请求可能重复执行
这类问题通常需要:
- 幂等键;
- 查询接口;
- 任务状态机;
- 延迟确认;
- 人工处理入口。
"超时就重试"不是默认正确答案。
3. 失败被默认值掩盖
例如:
ts
catch (error) {
return [];
}
这种写法有时很方便。
但你必须先确认:
text
空数组代表"真的没有数据"?
还是代表"查询失败了,但我们假装没数据"?
这两个语义完全不同。
如果调用方无法区分,后续业务判断就可能建立在错误信息上。
4. 权限或身份异常被降级处理
例如 AI 为了"保证流程继续",建议权限校验失败后返回默认权限、匿名状态或者可访问的空白数据。
这类做法风险很高。
权限异常、身份异常、签名校验失败,通常不应该被当作普通可恢复错误。
应该停止、记录、告警,并要求明确确认。
当你开始用 ChatGPT Plus 辅助补异常处理时,先带着一张检查清单
第一次用 ChatGPT Plus 这类工具帮助处理报错、补错误分支或整理异常日志时,最容易忽略的不是代码语法,而是"这个错误被捕获后,系统到底向谁承诺了什么"。
建议先用下面这份清单检查 AI 给出的方案:
text
【AI 辅助异常处理检查清单】
□ 这个异常发生后,当前业务动作到底有没有完成?
□ 返回成功时,是否真的存在可查询的业务结果或任务记录?
□ 默认值是否会把"失败"伪装成"没有数据"?
□ 超时后重试,是否可能造成重复执行?
□ 是否需要幂等键、任务状态或补偿机制?
□ 是否保留了 requestId、traceId 或可定位的错误码?
□ 是否向用户隐藏了内部细节,但向团队保留了排障证据?
□ 权限、身份、签名、金额、库存等异常是否被错误降级?
□ 测试是否覆盖"写入成功但后续失败"的中间状态?
□ 出现异常后,是否知道谁需要接手、如何回滚或补偿?
第一次把 AI 工具纳入开发工作流时,建议把使用说明、异常处理和必要信息留存一起写进检查清单;相关准备项可按需要参考:gpt328com
别为了排障方便,把完整生产日志直接丢给 AI
异常处理和排障经常会接触大量日志。
新人最容易做的一件事,是为了让 AI "看懂问题",直接复制整段线上日志、完整请求体、数据库错误、第三方回调信息。
这很不稳。
日志里可能包含:
- Token;
- Cookie;
- 手机号、邮箱等用户信息;
- 内部域名和服务地址;
- 订单号、支付标识;
- 数据库参数;
- 文件访问地址;
- 内部接口字段。
更稳的做法是先脱敏,再让 AI 做结构化分析。
例如:
text
原始内容:
user_id=82910022
token=eyJhbGci...
order_no=202606240001
callback_url=https://internal-pay.example.com/...
脱敏后:
user_id=user_xxx
token=[REDACTED]
order_no=order_xxx
callback_url=https://service-a/internal/...
AI 要解决的是调用链、异常类别和验证思路。
它通常不需要知道真实用户是谁,也不需要拿到完整凭证。
异常处理写完以后,至少验证这四件事
异常代码最容易"看起来完善"。
所以写完后不要只看编译通过。
至少要主动模拟下面四类情况。
| 场景 | 要验证什么 |
|---|---|
| 数据库写入失败 | 接口是否明确失败;是否没有假任务 ID;是否留下可追踪记录 |
| 队列投递失败 | 任务状态是否可恢复;是否需要补偿;前端是否被误导 |
| 外部调用超时 | 是否可能重复执行;是否有幂等保护;是否能后续确认状态 |
| 重复请求 | 是否创建重复任务;是否重复发送消息;返回结果是否一致 |
一个简单的测试示例可以这样写:
ts
it("队列投递失败时,不应返回任务已创建", async () => {
exportTaskRepository.create = vi.fn().mockResolvedValue({
id: "task_001",
status: "queued",
});
exportQueue.add = vi.fn().mockRejectedValue(
new Error("queue unavailable")
);
const result = await createExportTask({
userId: "user_001",
fileType: "csv",
});
expect(result.success).toBe(false);
if (!result.success) {
expect(result.code).toBe("EXPORT_QUEUE_UNAVAILABLE");
expect(result.retryable).toBe(true);
}
});
这段测试不代表所有异常处理都已经正确。
但它至少验证了一件关键事情:
队列失败时,系统没有向调用方伪造"任务已经开始"的事实。
最后:异常处理不是让失败消失,而是让失败保持可信
新人做异常处理时,最容易追求一个表面目标:
不要报错。 不要让页面崩。 不要让接口返回 500。
这些目标本身没有错。
但如果实现方式是把真实失败包成默认值、空结果、假状态、固定成功响应,那么问题只是被推迟了。
真正可靠的异常处理应该做到:
- 失败时不伪装成功;
- 可重试时明确说明为什么可重试;
- 不确定是否执行成功时保留状态,而不是猜测;
- 对用户隐藏内部细节,对团队保留排障证据;
- 对权限、金额、库存、身份等高风险异常保持严格边界;
- 让调用方知道下一步是等待、重试还是人工处理。
AI 可以帮你更快补齐异常分支,也可以帮你整理错误码、生成测试和列出边界条件。
但它不应该替你决定:
这次失败,系统能不能假装没发生过。
好的异常处理,不是让错误彻底消失。
而是让每一次失败都能被准确理解、可靠定位,并在该停下的时候停下来。
