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
相关产品推荐
相关产品推荐

