怎么在 Express 中间件中正确处理 async 函数抛出的异常?

文章导读
在 Express 4.x 版本中,async 函数内的错误不会自动传递给错误处理中间件,必须手动捕获并调用 next(err) 或使用包装函数。
📋 目录
  1. 快速处理思路
  2. 为什么会这样
  3. 分步处理
  4. 怎么验证是否生效
  5. 常见坑
A A

在 Express 4.x 版本中,async 函数内的错误不会自动传递给错误处理中间件,必须手动捕获并调用 next(err) 或使用包装函数。

先说结论:Express 4.x 不会自动捕获异步 Promise 拒绝,需通过包装函数或 try/catch 将错误显式传递给 next()

  • 先确认:检查项目使用的 Express 主版本,4.x 需手动处理,5.x 已原生支持
  • 先处理:使用 asyncHandler 包装异步中间件,或在函数内部 try/catch 后调用 next(err)
  • 再验证:触发异常后确认全局错误处理中间件是否接收到 err 对象且进程未崩溃

快速处理思路

如果不希望修改每个路由函数的内部逻辑,可以定义一个通用的包装函数,将异步错误自动转发给 Express 的错误处理流程。以下是一个常用的包装器实现:

const asyncHandler = (fn) => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};

// 使用方式
app.get('/user', asyncHandler(async (req, res) => {
  const user = await getUserById(req.params.id);
  res.json(user);
}));

如果不想引入额外函数,也可以直接在 async 函数内部使用 try/catch 块,并在 catch 中调用 next(err)。

为什么会这样

Express 4.x 设计时,异步流程主要基于回调函数,错误处理依赖同步抛错或手动调用 next(err)。当路由处理器是 async 函数时,它返回的是一个 Promise。如果 Promise 被拒绝(reject),Express 4.x 的路由层默认不会监听这个 Promise 的状态,导致错误变成“未处理的 Promise 拒绝”,进程可能因此退出或错误无法进入全局错误中间件。

Express 5.x 改进了这一机制,能够自动识别 async 函数返回的 Promise 并将其拒绝视为错误传递,但在 4.x 仍广泛使用的情况下,手动处理是必要的。

分步处理

1. 确认 Express 版本

查看 package.json 中的 express 依赖版本。如果是 4.x 系列,必须实施错误捕获方案。

2. 实现错误包装器

在项目工具目录创建一个 utils 文件,放入上文提到的 asyncHandler 函数。确保该函数接收一个函数 fn,并返回一个标准的 Express 中间件函数。

3. 包裹异步路由

将所有使用 await 的路由处理器用 asyncHandler 包裹。注意,同步中间件不需要包裹,只有可能返回 Promise 的函数才需要。

4. 确保全局错误中间件存在

检查应用最后是否注册了四个参数的错误处理中间件,这是接收错误的终点:

app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).send('Something broke!');
});

注意该函数必须有四个参数 (err, req, res, next),否则 Express 会将其视为普通中间件而非错误处理器。

怎么在 Express 中间件中正确处理 async 函数抛出的异常?

怎么验证是否生效

1. 制造异常

在某个被包裹的 async 路由中,手动 throw 一个新错误,或访问一个会导致数据库查询失败的接口。

2. 观察进程状态

确认 Node.js 进程没有因为 UnhandledPromiseRejectionWarning 而崩溃退出。

3. 检查响应与日志

客户端应收到 500 状态码(或你自定义的错误状态),服务器控制台应打印出错误堆栈,且由全局错误中间件统一输出,而不是由系统默认输出。

常见坑

1. 错误中间件参数不足

如果定义 error handler 时只写了三个参数 (req, res, next),Express 不会将其识别为错误处理中间件,错误会继续向下传递直到进程崩溃。

2. 混用同步与异步错误

包装函数只处理 Promise 拒绝。如果在 async 函数外部或同步代码段抛出错误,仍需依靠 Express 默认的同步捕获机制,确保不要漏掉非异步路径的错误。

3. next() 被重复调用

在 try/catch 模式下,确保 catch 块中调用 next(err) 后不再执行 res.send 或 res.json,否则会导致“ headers already sent”错误。

4. 升级 Express 5.x 后的变更

如果未来升级到 Express 5.x,原生的 async 支持可能使包装函数变得多余,甚至可能因双重捕获导致问题,升级时需移除手动包装逻辑。