每个接口都写 try catch 太烦:给 Express 加一层全局错误处理
我维护过一个几十个接口的 Node 服务,每个路由handler里都裹着 try catch,写起来重复,读起来也乱。更麻烦的是总有漏网的:某个异步查询抛了错没被接住,进程没崩但接口直接挂起,或者返回一个带着一堆调用栈的 500。后来把错误处理统一收拢到一个中间件里,代码干净了一半,出错时返回格式也终于统一了。
一、先定一个自己的错误类型

别到处 throw new Error(‘xxx’),那样最后没法区分该返回什么状态码。定义一个带状态码的错误类,业务里按情况抛它:
class HttpError extends Error {
constructor(status, message) {
super(message);
this.status = status;
}
}
// 用的时候
if (!user) throw new HttpError(404, '用户不存在');
if (!req.body.name) throw new HttpError(400, '缺少必填项 name');
这样错误信息自带该返回什么状态码,后面的中间件照着它出响应就行。
二、异步接口怎么把错丢出去
Express 4 时代,在 async 函数里抛的错误不会被它自己接住,因为 try catch 抓不到跨异步边界的异常。常见做法是包一层包装函数:
const asyncHandler = fn => (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next);
它把 handler 返回的 Promise 接住,一有 reject 就交给 next(err),走进错误处理流程。新版 Express 对 async 错误的处理已经友好了不少,但从兼容老版本的角度,包一层仍然是最稳的习惯。
app.get('/user/:id', asyncHandler(async (req, res) => {
const user = await findUser(req.params.id);
if (!user) throw new HttpError(404, '用户不存在');
res.json(user);
}));
handler 里只管正常流程和抛错,捕获的事交给包装函数,看着清爽多了。
三、404 和兜底错误中间件
两个中间件都注册在所有路由之后,顺序很讲究:
// 走到这说明路由都没匹配上
app.use((req, res, next) => next(new HttpError(404, '接口不存在')));
// 兜底:统一出错误响应
app.use((err, req, res, next) => {
console.error(err);
const status = err.status || 500;
res.status(status).json({
code: status,
message: err.status ? err.message : '服务器内部错误'
});
});
第二个中间件有四个参数,这是 Express 识别"错误中间件"的唯一依据,写成三个参数它就退化成普通中间件,永远不会被错误流程调用到,这点特别容易踩。里面把已知的 HttpError 按自己的 message 返回,未知的统统压成"服务器内部错误",避免把栈信息暴露给前端。
四、踩坑提醒
第一,注册顺序必须是最后。错误中间件放在路由前面,请求还没进 handler 就被它拦了,等于接口全废。规矩是:路由 → 404 中间件 → 错误中间件,一路排下来。
第二,四个参数一个都不能少。哪怕你不打算用 next,签名也得是 (err, req, res, next),少一个 Express 就不认它是错误处理器。
第三,别把原始错误抛给前端。生产环境返回统一的文案和错误码就好,详细信息打进日志,回头查问题有的是地方看。这既是安全习惯,也让前端好处理。
第四,处理不了的全局异常要单独兜一层。process.on(‘uncaughtException’) 和 unhandledRejection 建议都监听一下,记录完日志体面退出,交给守护进程拉起来,别让进程僵在半死不活的状态。
第五,别指望它替代参数校验。每个接口的入参还是要认真校验,能提前挡住的别等到报错处理,错误中间件是最后一道防线,不是唯一一道。
第六,res.json 之后再抛错是没用的,响应已经发出去了。这种"晚到的错误"只能靠日志发现问题。
五、小结
统一错误处理的收益很直接:handler 里少写一堆 try catch,返回格式前后一致,排查问题时日志集中在同一个出口。记住三个关键点就行:自定义错误带状态码、async 用包装函数把错转给 next、错误中间件放最后且必须四个参数。这套东西不难,但一旦习惯了,回头看满屏 try catch 的代码会很难受。
- 点赞
- 收藏
- 关注作者
评论(0)