自定义一套统一格式的异常/错误返回 让前端更好接

2026-09-24 75 浏览 0 评论 0 字

统一异常返回的核心是「一套所有接口都遵循的响应结构 + 自己的业务错误类型」。本文讲解 code + message + data 响应体的设计,区分业务码与 HTTP 状态码的分工,演示如何定义业务异常类和异常兜底中间件,让前端只认一套健壮约定,不再零散地写 if (data.code === xxx) 。

问题背景

前端对接接口时最怕什么?怕后端报错格式五花八门。有的接口失败时返回 { code: 0, data: null } ,有的直接 res.send('error') ,有的干脆红了眼返回一个 HTML 错误页。前端只能每个接口单独写异常分支,代码里全是 if (data.code === xxx) 的零散判断。问题的根源是后端没有一个统一的错误协议。

原理说明

统一错误返回的核心是: 定义一套所有接口都遵循的响应结构,并为业务错误建立自己的错误类型 。通用的响应体通常长这样:

{
  "code": 0,
  "message": "ok",
  "data": null
}

其中 code 是业务码, 0 表示成功,非 0 表示各类业务错误(比如 1001 参数不合法、1002 未登录、4040 资源不存在)。HTTP 状态码用于表达"请求本身的语义"(4xx 客户端问题、5xx 服务端问题), code 用于表达"业务层面"的具体错误,二者分工不同。

实现上分两步:

  1. 定义自己的业务异常类,携带业务码和可读信息;
  2. 在异常兜底中间件里统一把异常转换成上面那个结构,前端只认这一套。

示例代码

定义统一的业务异常:

// utils/BusinessError.js
// 业务错误:带 code 和可给用户看的 message
class BusinessError extends Error {
  constructor(code, message, httpStatus = 200) {
    super(message);
    this.code = code;        // 业务码,如 1001
    this.httpStatus = httpStatus;  // 配套的 HTTP 状态码
  }
}

// 快速构造
BusinessError.badRequest = (msg) => new BusinessError(400, msg, 400);
BusinessError.notFound = (msg) => new BusinessError(4040, msg, 404);
BusinessError.unauthorized = () => new BusinessError(1002, '请先登录', 401);

module.exports = BusinessError;

统一响应工具:

// utils/httpResponse.js
function ok(res, data = null, message = 'ok') {
  res.json({ code: 0, message, data });
}

function fail(res, code, message, httpStatus = 200) {
  res.status(httpStatus).json({ code, message, data: null });
}

module.exports = { ok, fail };

异常兜底中间件把它们衔接起来:

// middlewares/errorHandler.js
const BusinessError = require('../utils/BusinessError');

module.exports = function errorHandler(err, req, res, next) {
  // 已经响应过,直接跳过
  if (res.headersSent) return next(err);

  if (err instanceof BusinessError) {
    return res.status(err.httpStatus).json({
      code: err.code, message: err.message, data: null,
    });
  }

  // 未料到的异常:记日志,给前端一个通用提示
  console.error('unexpected error:', err);
  res.status(500).json({ code: 500, message: '服务器内部错误', data: null });
};

业务里这样抛:

// services/orderService.js
const BusinessError = require('../utils/BusinessError');

async function getOrderById(id) {
  const order = await findById(id);
  if (!order) throw BusinessError.notFound('订单不存在');
  return order;
}

常见坑与解决方案

  • 坑一:把 HTTP 状态码和业务码混成一个。解法:分开存,HTTP 语义归 HTTP,业务细分归 code,前端以 code 为主判断逻辑。
  • 坑二:统一异常中间件没放在路由之后,导致路由里的异常根本没走到。解法: errorHandler 必须注册在所有路由之后(最后面)。
  • 坑三: res.headersSent 判断缺失,响应已发出又去写内容导致报错。解法:兜底函数里先判断 headersSent ,已发出就直接 next 。
  • 坑四:给前端的异常信息泄露了内部细节(如 SQL 语句)。解法:给用户的 message 统一用友好文案,内部细节只打到服务端日志。

小结

一个 code + message + data 的统一结构,配一个业务错误类和中间件兜底,前端就能只认一套健壮的约定。Service 抛什么,出去的永远是规范的东西。接口稳了之后,该考虑的是进程本身怎么优雅地收尾和退出。

FAQ 常见问题

Q:业务码 code 和 HTTP 状态码为什么得分开放?
A:HTTP 状态码表达请求本身的语义(4xx/5xx), code 表达业务层面的具体错误(如 1001 参数不合法)。两者分工不同,前端以 code 为主判断业务逻辑,独立编码才不混淆。

Q:统一异常中间件为什么没生效?
A:很可能是注册顺序不对。 errorHandler 必须放在所有路由之后(最后面),否则路由里抛出的异常根本走不到它。

Q:兜底时为什么先判 res.headersSent ?
A:如果响应已经发出(headersSent 为 true),再写内容会报错。所以先判断已发出就直接 next(err) 交给上层处理,避免二次响应。


发布评论

发布评论前请先 登录。
0 评论
点赞
收藏

评论列表 0

暂无评论