后端接口错误码和异常处理怎么设计

域名注册
域名注册 正式会员超兽战士 👑年卡会员
发布于 2026-10-07 19:29 ·1 浏览 ·0 回复

结论:后端接口的错误处理按「HTTP 状态码管传输语义 + 业务错误码管具体原因 + 统一响应体管字段结构」三层来做,业务代码里只负责抛异常,不负责拼错误响应,所有异常在全局处理器收敛成同一种 JSON。这套结构定下来之后,前端判断分支、日志排查、监控告警三件事会同时变简单。

错误码为什么要分三层,各层管什么?

结论:HTTP 状态码表达「这次调用在协议层面发生了什么」,业务错误码表达「哪条业务规则被违反」,message 只给人看,三者职责不能混。

  • 第一层 HTTP 状态码:401 未认证、403 无权限、404 资源不存在、409 状态冲突(如重复提交)、422 参数校验失败、429 触发限流、500 服务端未捕获异常、503 依赖服务不可用。
  • 第二层业务错误码:全局唯一整数,例如 10201 表示「余额不足」。
  • 第三层 message + details:message 面向用户或调用方,details 放字段级校验信息,例如 {"field":"phone","reason":"格式错误"}。

最常见的错误做法是所有情况都返回 200,把失败信息塞进 body。这会让网关、重试组件、浏览器缓存、监控系统全部失效——它们只认 HTTP 状态码。

业务错误码的编码规则怎么定?

结论:用定长数字、分段编码,并把分配规则写进文档后冻结,错误码只能新增、禁止复用和改语义。

推荐 6 位方案:第 1 位表示错误归属(1 为客户端问题、2 为服务端问题、3 为第三方依赖问题),第 2-3 位表示模块,第 4-6 位表示模块内序号。举例:100101 用户模块参数错误,200301 订单模块库存不足,300102 支付网关超时。0 保留给成功。

为什么要定长分段:前端可以按前缀做粗粒度兜底(code / 100000 === 2 就弹「服务异常,请稍后重试」),SRE 可以按模块前缀做告警聚合。如果错误码是流水号,这两件事都做不了。

统一响应体应该包含哪些字段?

结论:固定为 code、message、data、traceId、timestamp 五个字段,多一个都别加。

{
  "code": 0,
  "message": "ok",
  "data": { "orderId": "20240520001" },
  "traceId": "a1b2c3d4e5f6",
  "timestamp": 1716182400000
}

成功时 code 为 0,失败时为非 0 业务码。traceId 必须透传到日志和下游调用,这是排查线上问题的唯一抓手——没有它,用户截图里的报错你只能靠时间戳猜。

业务代码里异常怎么抛?

结论:业务层只抛自定义异常,不在 Controller 里 return 错误对象,也不 try-catch 后返回 null。

// 正确:抛出携带错误码的异常
if (balance < amount) {
    throw new BizException(ErrorCode.INSUFFICIENT_BALANCE);
}
// 错误:controller 里手动拼错误响应
return Result.fail(10201, "余额不足");

错误码用枚举定义,ErrorCode 里同时持有 code 和默认 message。这样做的好处是错误码集中在一处,改文案不用改业务逻辑,编译期就能发现拼错的码。

全局异常处理器怎么落地?

结论:用 Spring 的 @RestControllerAdvice 或 Go 的 recovery middleware 收敛所有异常,分三类处理。

@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(BizException.class)
    public ResponseEntity<Result<?>> handleBiz(BizException e) {
        return ResponseEntity.status(httpStatusOf(e.getCode()))
                .body(Result.fail(e.getCode(), e.getMessage()));
    }
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Result<?>> handleValid(MethodArgumentNotValidException e) {
        // 参数校验统一映射 422,details 带字段级原因
    }
    @ExceptionHandler(Exception.class)
    public ResponseEntity<Result<?>> handleOther(Exception e) {
        log.error("uncaught, traceId={}", TraceContext.get(), e);
        return ResponseEntity.status(500).body(Result.fail(50000, "服务内部错误"));
    }
}

关键点:BizException 的 code 映射到对应的 HTTP 状态码,校验类异常固定 422,兜底 Exception 固定 500 且只返回「服务内部错误」,堆栈只进日志。

必须避开的坑有哪些?

结论:五个高频错误,任何一个都会在半年后变成线上事故。

  1. 把异常堆栈或 SQL 语句返回给前端,泄漏表结构和文件路径。
  2. 复用或修改已有错误码语义,客户端硬编码的判断全部失效。
  3. 参数校验失败返回 500,触发无关的告警和重试。
  4. 日志里不带 traceId,多实例部署时无法串起一次调用。
  5. 只给 message 不给 code,前端只能靠字符串匹配判断错误类型,改一个字就崩。

一句话收束:把状态码、业务码、响应体三层的职责切干净,业务代码只抛异常、全局处理器只做翻译、traceId 全程透传,接口错误处理这件事就算设计对了。

版权声明:本文来自 GJ站长论坛《后端接口错误码和异常处理怎么设计》
原文链接:https://www.gj0.com/thread-495.html
转载请注明出处并保留本声明;内容仅代表作者观点,与本站立场无关。若本文涉嫌侵权,请联系本站处理。

全部回复 0

还没有回复,来抢沙发~