Codex重构异常处理为什么容易把错误“吃掉”?别让try/catch掩盖真正Bug 使用 Codex 修复项目报错时经常会看到一种很“有效”的修改原来程序会直接抛出异常修改以后页面不报错了、接口也不再返回 500测试甚至可能重新变绿。但继续使用一段时间后却会发现新的问题接口明明失败了前端却显示“暂无数据”数据库写入失败业务仍然返回成功日志里只剩一句Something went wrong外部接口超时后被自动忽略真实 Bug 没有解决只是异常不再显示同一个错误被重复重试几十次线上数据出现异常却找不到第一次失败的位置。这种情况通常可以称为错误被吞掉了。Codex 并不是不会处理异常而是如果任务只要求“不要再报错”最简单的实现往往就是增加try/catch。但“程序不报错”和“问题被正确处理”完全是两件事。一、最危险的catch什么都不做例如try { await saveOrder(order); } catch (error) { // ignore }从程序运行角度看它确实不会继续抛异常。但假设saveOrder()因数据库连接失败没有保存订单后面的代码仍然继续执行await sendOrderCreatedNotification(order);最终可能出现数据库没有订单 用户收到订单创建成功通知系统状态已经不一致。所以一个异常是否应该捕获首先要回答当前这一层真的知道应该怎么处理这个错误吗如果不知道通常就不应该直接吞掉。二、catch后return null也可能在隐藏问题另一种常见写法async function getUser(id: string) { try { return await userRepository.findById(id); } catch (error) { return null; } }调用方看到null后会认为用户不存在但真实原因可能是数据库连接失败 SQL执行异常 连接池耗尽这样就把两类完全不同的状态混在一起了正常业务结果用户不存在 系统错误数据库查询失败更合理的写法是明确区分async function getUser(id: string) { try { return await userRepository.findById(id); } catch (error) { throw new UserQueryError( Failed to query user, { cause: error } ); } }业务层可以继续判断真正的“用户不存在”而系统异常则进入统一错误处理。三、不要把所有异常都转成同一个错误Codex 为了统一接口返回有时会生成try { await service.run(); } catch { throw new Error(Operation failed); }这样虽然看起来整洁却丢失了最重要的信息。原始错误可能分别是ValidationError PermissionError DatabaseTimeoutError ExternalApiError但最后全部变成Operation failed线上排查时几乎无法判断到底发生了什么。更合理的做法是保留错误类型和原始原因throw new OrderCreateError( Failed to create order, { cause: error, orderId } );这样既可以提供业务上下文又不会丢失底层异常链。四、建立明确的错误类型对于中大型项目可以将异常大致分成几类。1. 参数错误例如用户名为空 分页参数小于0 文件类型不支持通常属于客户端可修正问题。可以返回400 Bad Request2. 身份与权限错误例如没有登录 Token失效 没有操作权限分别对应401 4033. 资源状态错误例如订单不存在 订单已经取消 库存不足属于业务规则的一部分。4. 基础设施错误例如数据库超时 Redis不可用 第三方接口失败这种错误通常不能简单伪装成“数据不存在”。5. 未知错误真正没有预料到的异常需要进入统一日志和告警流程。错误分类清晰以后Codex 才不容易用一个catch处理所有问题。五、只在“能真正处理”的层级捕获错误一个很实用的原则是谁能做出有效决策谁才捕获错误。例如 Repository 层async function findUser(id: string) { return db.user.findUnique({ where: { id } }); }如果数据库发生错误Repository 不一定需要立刻catch { return null; }因为它无法判断上层业务希望怎么处理。Service 层可能更清楚const user await userRepository.findUser(id); if (!user) { throw new UserNotFoundError(id); }而 API 层负责最终转换UserNotFoundError → 404 PermissionDeniedError → 403 ValidationError → 400未知异常则统一返回500这样错误处理链会更清晰。六、不要在每一层都重复记录同一个错误另一个常见问题是Repositorylogger.error(error); throw error;Servicelogger.error(error); throw error;Controllerlogger.error(error); throw error;最终一条异常可能生成三四条几乎相同的日志。线上看到ERROR database timeout ERROR database timeout ERROR database timeout反而无法判断哪个才是真正的错误入口。更好的做法是底层增加必要上下文最终统一错误边界记录一次完整日志中间层只在真正增加业务信息时记录。例如统一输出traceId errorType operation userId orderId cause duration而不是每层都console.error()。七、重试不是所有错误的解决方案Codex 遇到网络错误时很容易建议自动重试。但下面这些错误不应该重试400 参数错误 401 未认证 403 无权限 404 资源不存在 业务状态不允许这些问题重试多少次结果都一样。更适合重试的是临时性错误网络抖动 连接超时 429限流 部分5xx错误 临时数据库连接异常可以明确写function isRetryable(error: unknown) { return ( error instanceof NetworkTimeoutError || error instanceof RateLimitError ); }而不是catch { retry(); }八、禁止无限递归重试这种代码风险很高async function request() { try { return await api.call(); } catch { return request(); } }如果服务持续不可用就会无限重试。更合理的是设置最大次数 等待时间 错误类型 最终失败状态例如for (let attempt 1; attempt 3; attempt) { try { return await api.call(); } catch (error) { if (!isRetryable(error) || attempt 3) { throw error; } await sleep(attempt * 1000); } }重试的目标是处理暂时故障而不是让错误永远无法暴露。九、finally也可能带来隐藏Bugfinally无论成功还是失败都会执行。例如try { await saveData(); } finally { setStatus(completed); }即使saveData()报错状态仍然会变成completed这显然不正确。更合理的是try { await saveData(); setStatus(completed); } catch (error) { setStatus(failed); throw error; } finally { setLoading(false); }finally更适合关闭连接 释放锁 停止Loading 清理临时资源而不是写业务成功状态。十、前端不要把所有异常显示成“网络错误”接口可能返回400 参数错误 403 权限不足 409 状态冲突 429 请求过快 500 服务异常如果前端全部显示网络异常请稍后重试用户和开发者都会失去重要信息。可以建立错误映射switch (error.code) { case ORDER_ALREADY_CANCELLED: return 订单已经取消; case PERMISSION_DENIED: return 当前账号没有操作权限; default: return 系统暂时无法完成操作; }用户提示可以友好但日志仍应保留真实错误信息。十一、让Codex先画出错误传播链处理异常问题时可以先要求请先不要修改代码。 分析当前错误传播链 1. 错误最初在哪里产生 2. 经过哪些函数 3. 哪一层第一次catch 4. 是否修改了错误类型 5. 是否存在return null / [] / false 6. 是否发生重复日志 7. 最终API返回什么。很多“偶发Bug”其实一看传播链就能发现DatabaseError ↓ catch ↓ return null ↓ 被当成用户不存在 ↓ 返回404真实数据库故障就这样被隐藏了。十二、测试必须验证错误路径不要只测试数据库正常 → 用户查询成功还应该测试数据库超时 → 不能返回404 权限不足 → 必须返回403 参数错误 → 不允许继续调用数据库 外部接口失败 → 不能返回成功状态 重试达到上限 → 必须暴露最终错误例如it( does not convert database failure to user not found, async () { repository.findUser.mockRejectedValue( new DatabaseTimeoutError() ); await expect( service.getUser(1001) ).rejects.toThrow(DatabaseTimeoutError); } );这种测试可以防止未来 Codex 再次为了“让接口稳定”而吞掉真实错误。十三、把异常处理规则写进AGENTS.md可以加入# 异常处理规则 - 禁止空catch - 禁止无理由return null掩盖系统错误 - 保留原始error cause - 业务错误与系统错误必须区分 - 只有可恢复错误允许自动重试 - 所有重试必须设置最大次数 - finally只用于清理资源不表示业务成功 - 不允许为了消除500直接吞掉异常 - 修改异常流程后必须增加失败路径测试这类规则对 Codex 很有帮助。因为模型在修复问题时会更明确目标不是让错误消失而是让错误被正确处理。十四、一个推荐的错误处理结构可以采用Repository ↓ 产生底层错误 ↓ Service 增加业务上下文 ↓ Controller / Error Boundary 转换成统一响应 ↓ Logger 记录一次完整异常 ↓ 用户 看到安全、可理解的提示错误应该被转换而不是被消失。十五、Plus还是Pro如果主要是单接口异常处理 普通Bug排查 简单try/catch重构 少量失败测试Plus 通常足够。如果需要长期处理大型仓库、复杂调用链、微服务错误传播、大量日志和多轮测试则可以根据实际开发强度评估 Pro。不过更大的使用空间只能帮助连续分析。错误是否被正确处理最终还是取决于项目自己的异常边界设计。总结Codex 重构异常处理以后程序“不再报错”并不一定是好事。真正危险的是错误发生了 ↓ 被catch ↓ 被转换成null ↓ 业务继续运行 ↓ 系统表面正常通过错误分类、明确传播边界、保留原始 cause、限制重试和覆盖失败路径测试可以避免try/catch变成隐藏 Bug 的工具。真正可靠的异常处理不是让系统永远不出现错误而是确保每个错误发生以后能够被识别、被记录、被正确响应并且不会悄悄破坏后续业务状态。CSDN文章描述本文介绍 Codex 重构异常处理时常见的“吞错”问题通过错误类型、传播边界、重试规则、统一日志和失败路径测试避免 try/catch 掩盖真实 Bug。