服务端与数据 / 进阶

Express 异步错误处理:让异常只经过一个出口

把业务异常、参数错误和未知故障收敛到统一中间件,避免重复 try/catch、泄露堆栈和一条请求记录多次日志。

先区分可预期错误和程序故障

资源不存在、库存不足和参数不合法属于可预期的业务结果;数据库连接中断、空指针和断言失败属于程序故障。前者需要稳定的错误码,后者需要完整日志,但客户端都不应该看到内部堆栈。

EXAMPLE / 01Express
export class AppError extends Error {
  constructor(message, { status = 500, code = 'INTERNAL_ERROR', details } = {}) {
    super(message);
    this.name = 'AppError';
    this.status = status;
    this.code = code;
    this.details = details;
  }
}

路由只负责抛出错误

在 Express 5 中,被 async 路由拒绝的 Promise 会自动交给错误中间件。Express 4 则需要包装器调用 next。无论哪个版本,都不要在每个路由里重复记录和返回错误。

EXAMPLE / 02Express
app.get('/users/:id', async (req, res) => {
  const user = await userService.findById(req.params.id);
  if (!user) {
    throw new AppError('用户不存在', { status: 404, code: 'USER_NOT_FOUND' });
  }
  res.json({ data: user });
});

在最后一个中间件统一响应

错误中间件记录完整异常;上层网关只记录状态、耗时和 requestId。这样既保留上下文,也不会让同一故障生成多条重复告警。

EXAMPLE / 03Express
app.use((err, req, res, next) => {
  if (res.headersSent) return next(err);

  const known = err instanceof AppError;
  const status = known ? err.status : 500;
  req.log[status >= 500 ? 'error' : 'warn']({ err, requestId: req.id });

  res.status(status).json({
    error: {
      code: known ? err.code : 'INTERNAL_ERROR',
      message: known ? err.message : '服务暂时不可用',
      requestId: req.id,
      ...(known && err.details ? { details: err.details } : {})
    }
  });
});