You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Express中使用async/await的REST API错误处理最佳实践

现有实现的合理性评估

当前的错误处理实现并不合理,存在以下几个明显问题:

  • 状态码使用不符合语义:用户不存在属于资源未找到场景,应该返回404状态码而非400;catch块中不区分错误类型一律返回500,比如传入的ID格式不符合MongoDB ObjectId规则触发的CastError本质是客户端参数错误,应该返回400。
  • 存在敏感信息泄露风险:catch块直接将原生错误对象序列化为JSON返回给前端,会暴露数据库错误详情、服务端堆栈信息、内部结构等敏感内容,生产环境存在严重安全隐患。
  • 样板代码冗余:每个路由都手写try/catch块,接口数量增多后会产生大量重复代码,且一旦漏写try/catch,async路由函数抛出的异常不会被Express内置错误处理器捕获,会直接导致服务进程崩溃。
  • 错误响应格式不统一:分支逻辑中返回{message: "xxx"}格式,catch块直接返回原生错误对象,前端需要适配多种错误结构,增加了对接成本,对于即时通讯这类需要全局统一错误提示的场景非常不友好。
推荐的错误处理最佳实践

1. 封装异步路由错误捕获工具

通过高阶函数统一包裹async路由,自动捕获异常传递给全局错误中间件,彻底消除每个路由手写try/catch的冗余代码:

// 异步错误捕获包装函数
const catchAsync = (handler) => (req, res, next) => {
  Promise.resolve(handler(req, res, next)).catch(next);
};

使用时直接包裹路由处理函数即可:

router.get("/:id", catchAsync(async ({ params }, res) => {
  const user = await User.findById(params.id);
  if (!user) {
    throw new AppError(404, "用户不存在");
  }
  res.json(user);
}));

2. 自定义业务错误类

封装统一的错误类,用来区分可对外暴露的业务错误和未知系统错误,附带状态码、错误类型等标识:

class AppError extends Error {
  constructor(statusCode, message, isOperational = true) {
    super(message);
    this.statusCode = statusCode;
    // 4xx为客户端错误,5xx为服务端错误
    this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
    // 标记为可预期的业务错误,可直接向前端返回信息
    this.isOperational = isOperational;
    Error.captureStackTrace(this, this.constructor);
  }
}

3. 实现全局统一错误处理中间件

在所有路由之后挂载全局错误中间件,统一处理所有抛出的错误,区分开发/生产环境返回不同粒度的信息,同时自动适配常见的数据库、参数校验错误:

app.use((err, req, res, next) => {
  err.statusCode = err.statusCode || 500;
  err.status = err.status || 'error';

  // 开发环境返回完整错误信息,方便调试
  if (process.env.NODE_ENV === 'development') {
    return res.status(err.statusCode).json({
      status: err.status,
      message: err.message,
      stack: err.stack,
      rawError: err
    });
  }

  // 生产环境处理常见的框架/数据库错误,转换为友好提示
  let handledErr = { ...err };
  handledErr.message = err.message;
  // MongoDB ObjectId格式错误
  if (err.name === 'CastError') {
    handledErr = new AppError(400, `无效的参数${err.path}: ${err.value}`);
  }
  // 数据库唯一索引冲突
  if (err.code === 11000) {
    const conflictKey = Object.keys(err.keyValue)[0];
    handledErr = new AppError(400, `${conflictKey}已被占用`);
  }
  // Mongoose参数校验错误
  if (err.name === 'ValidationError') {
    const errMsgs = Object.values(err.errors).map(item => item.message);
    handledErr = new AppError(400, `参数校验失败: ${errMsgs.join(', ')}`);
  }

  // 可预期的业务错误,直接返回提示
  if (handledErr.isOperational) {
    return res.status(handledErr.statusCode).json({
      status: handledErr.status,
      message: handledErr.message
    });
  }

  // 未知系统错误,记录日志后返回通用提示,不暴露内部细节
  console.error('未预期的系统错误:', err);
  return res.status(500).json({
    status: 'error',
    message: '服务器内部异常,请稍后重试'
  });
});

4. 补充边界防护

  • 严格遵循HTTP状态码语义:资源不存在返回404、未登录返回401、无权限返回403、参数错误返回400、服务端内部异常才返回500
  • 保持错误响应结构统一,固定返回status、message字段,方便前端全局拦截处理,适配即时通讯场景的全局提示、登录态失效自动跳转等通用逻辑
  • 添加进程级异常监听,避免未捕获异常直接导致服务崩溃:
// 捕获同步代码未处理的异常
process.on('uncaughtException', (err) => {
  console.error('未捕获的同步异常:', err);
  // 可在此处添加错误日志上报、资源释放逻辑,之后优雅重启
  process.exit(1);
});

// 捕获未被处理的Promise异常
process.on('unhandledRejection', (err) => {
  console.error('未处理的Promise拒绝:', err);
  server.close(() => process.exit(1));
});

内容的提问来源于stack exchange,提问作者Alen

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 01:42:28