AI 帮你补异常处理时,新人最容易犯的错:把失败悄悄变成成功

新人第一次让 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",
  };
}

这段逻辑里至少有两个可能失败的位置:

  1. 创建任务记录失败;
  2. 任务记录成功创建,但投递队列失败。

新人让 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。

这些目标本身没有错。

但如果实现方式是把真实失败包成默认值、空结果、假状态、固定成功响应,那么问题只是被推迟了。

真正可靠的异常处理应该做到:

  1. 失败时不伪装成功;
  2. 可重试时明确说明为什么可重试;
  3. 不确定是否执行成功时保留状态,而不是猜测;
  4. 对用户隐藏内部细节,对团队保留排障证据;
  5. 对权限、金额、库存、身份等高风险异常保持严格边界;
  6. 让调用方知道下一步是等待、重试还是人工处理。

AI 可以帮你更快补齐异常分支,也可以帮你整理错误码、生成测试和列出边界条件。

但它不应该替你决定:

这次失败,系统能不能假装没发生过。

好的异常处理,不是让错误彻底消失。

而是让每一次失败都能被准确理解、可靠定位,并在该停下的时候停下来。

相关推荐
AlfredZhao3 天前
GPT 省钱,不是别用最新模型,而是别浪费缓存
gpt·ai
凌奕3 天前
让你的 AI 编程助手「偷懒」:50k Star 的 Ponytail,让 Agent 少写一半代码
chatgpt·agent·claude
newbe365247 天前
对接 Reasonix 1.x 跑通 DeepSeek V4:ACP 模型选择器接入实战
gpt·claude·chatglm (智谱)
newbe365248 天前
如何使用 Upptime 免费搭建自己的状态站点
gpt·claude·chatglm (智谱)
gis分享者9 天前
GPT-Image-2 图像生成模型新手实战指南
gpt·ai·image·模型·图像生成
星落zx9 天前
Spring Boot 多模型集成:优雅调用全球主流大模型
人工智能·spring boot·chatgpt