Express错误处理最佳实践与全局异常捕获中间件实现方法
先解释你示例代码的问题
你的判断完全正确。Express 中间件按注册顺序依次执行,只要前面的路由或中间件已经通过res.send()等方法结束了响应、且没有主动调用next(),后续注册的中间件就不会执行。你写的最后一个中间件本质是兜底逻辑,只有当前面所有路由都没匹配到请求、或者前置逻辑主动放行时才会触发,正常命中路由返回响应的场景下确实不会执行。
Express 错误处理的标准最佳实践
Express 有专门的错误处理中间件规范,不需要你在每个路由里重复写try/catch,核心规则和实现步骤如下:
- 错误处理中间件必须固定包含4个参数
(err, req, res, next),少一个参数Express就不会把它识别为错误处理组件,必须注册在所有普通中间件、所有路由的最后面。
注意:哪怕你在中间件里用不到next参数,也必须在形参里写全4个参数,否则Express会把它当成普通中间件处理,接不到错误对象 - 同步代码抛出的错误,Express 会自动捕获并传递到错误处理中间件,不需要手动处理。
- 异步代码(包括async/await、回调风格的异步逻辑)在Express 4.x版本中不会被自动捕获,你只需要写一个通用的高阶包装函数,就不用每个路由重复写捕获逻辑:
// 异步路由通用包装器,所有async路由直接套这个就行 const asyncWrap = (routeHandler) => (req, res, next) => { Promise.resolve(routeHandler(req, res, next)).catch(next) } // 使用示例 app.get('/user', asyncWrap(async (req, res) => { // 这里不管是await异步操作抛错,还是同步throw错误,都会自动传到全局错误中间件 const user = await db.User.findById(req.query.id) res.send(user) }))
如果是用Express 5.x版本,异步错误已经做了自动捕获,连这个包装器都不需要,直接写async路由就行,抛错会自动进错误中间件。
- 在所有路由之后、错误处理中间件之前,加一个404兜底中间件,把未匹配到路由的请求统一转成错误传给后续处理:
app.use((req, res, next) => { const notFoundErr = new Error(`路径 ${req.method} ${req.path} 不存在`) notFoundErr.statusCode = 404 next(notFoundErr) })
- 最后写全局错误处理中间件,你需要的告警逻辑、统一错误返回都放在这里,所有错误都会汇总到这:
app.use((err, req, res, next) => { // 统一告警逻辑:发邮件、推监控webhook、打错误日志全写在这,只需要写一次 console.error('[应用错误]', err) // 只有5xx类的服务端错误触发告警,4xx类的客户端请求错误不需要告警 if (!err.statusCode || err.statusCode >= 500) { // 替换成你自己的邮件、告警推送逻辑 sendCrashAlert(err) } // 统一返回错误响应给客户端 const status = err.statusCode || 500 res.status(status).json({ msg: process.env.NODE_ENV === 'production' ? status >=500 ? '服务异常,请稍后重试' : err.message : err.message, // 开发环境可以返回错误栈方便排查,生产环境不要返回 stack: process.env.NODE_ENV === 'development' ? err.stack : undefined }) })
- 额外补一层进程级的错误监听,兜底捕获完全没被Express接住的异常,避免应用直接崩溃没告警:
// 捕获全局未处理的同步异常 process.on('uncaughtException', (err) => { console.error('[全局未捕获异常]', err) sendCrashAlert(err) // 这类异常出现后建议退出进程,用pm2、k8s等工具自动重启服务,避免内存泄漏等问题 process.exit(1) }) // 捕获全局未处理的Promise异常 process.on('unhandledRejection', (err) => { console.error('[未处理Promise异常]', err) sendCrashAlert(err) })
对你现有方案的评价
你现在在每个路由里单独写try/catch、单独触发告警的方式能跑,但完全不是最优解,问题很明显:
- 重复代码太多,每加一个路由就要写一遍相同的捕获、告警、返回逻辑
- 非常容易漏写,只要有一个异步路由没包
try/catch,抛错就会直接打挂进程,告警根本触发不了 - 错误返回格式很难统一,不同路由可能返回不同结构的错误信息,前端对接成本高
- 后续要改告警规则、改返回格式的时候,要改所有路由的代码,维护成本极高
用上面的全局错误处理方案,你可以把所有路由里的try/catch全删掉,所有错误都会自动汇总到全局中间件,告警逻辑只需要维护一份,也不会出现漏捕获的情况。
内容的提问来源于stack exchange,提问作者Ali Abyer
相关产品推荐
相关产品推荐

