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

Express中处理unhandledRejection与uncaughtException避免ERR_HTTP_HEADERS_SENT问题

问题根因

你现在的代码设计从根上存在逻辑错误,这是触发ERR_HTTP_HEADERS_SENT报错的核心原因:

  • unhandledRejection和uncaughtException是进程级别的全局事件,和单个HTTP请求没有绑定关系。你在每个请求的中间件里注册全局事件监听器,相当于每进来一个请求就给进程多挂一个监听器,当有任意一个未处理的Promise rejection触发时,所有当前活跃请求绑定的监听器都会同时执行。
  • 每个监听器都绑定了当前请求的响应对象res,就算你判断了headersSent,也会出现两种问题:一是错误的把不属于当前请求的全局异常,当成当前请求的错误返回给客户端,出现逻辑错乱;二是高并发场景下判断headersSent和发送响应之间存在竞态条件,还是可能触发重复发送响应的报错。
  • 额外的问题:你这种写法会导致内存泄漏,每个请求的req和res都会被监听器引用,请求结束后无法被垃圾回收,进程内存会持续上涨。
正确解决方案

1. 拆分全局异常处理和请求级错误处理

全局事件监听器只需要在进程启动时注册一次,仅做日志上报、优雅退出等进程级逻辑,不要操作任何请求响应对象:

// 全局未处理Promise rejection捕获,仅注册一次
process.on('unhandledRejection', (reason, promise) => {
  self.logger.error("[unhandledRejection] 错误码:%d 信息:%s 调用栈:\n%@", reason.code, reason.message, reason.stack);
  // 可在此处添加上报、优雅退出逻辑,不要操作请求响应
});

// 全局未捕获异常捕获,仅注册一次
process.on('uncaughtException', (err) => {
  self.logger.error("[uncaughtException] 错误码:%d 信息:%s 调用栈:\n%@", err.code, err.message, err.stack);
  // 未捕获异常会导致进程状态异常,建议上报完成后退出进程
  process.exit(1);
});

2. 使用Express官方错误处理中间件处理请求级错误

Express自带专门的错误处理中间件(要求是四个参数,第一个为错误对象),放在所有路由和普通中间件之后注册即可统一处理所有请求链上的错误:

// Express 错误处理中间件
app.use((err, req, res, next) => {
  self.logger.error("[请求处理错误] 路径:%s 错误:%s", req.path, err.message);
  if (res.headersSent) {
    return next(err);
  }
  const errorMessage = APIHelper.createErrorMessage(500, '', '', err.message);
  res.status(500).send(errorMessage);
});

3. 适配异步路由的错误传递

如果你使用的是Express 4.x版本,异步路由中抛出的错误不会自动传递给错误处理中间件,需要手动封装一个异步处理器包装路由逻辑:

// 异步路由错误捕获包装器
const asyncHandler = (fn) => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};

// 路由使用示例
app.get('/api/xxx', asyncHandler(async (req, res) => {
  // 异步逻辑抛出的错误会自动传给错误处理中间件
  const result = await someAsyncOperation();
  res.send(result);
}));

如果你使用的是Express 5.x及以上版本,无需额外包装,异步路由的错误会自动传递给错误处理中间件。

临时兼容方案

如果暂时无法重构现有逻辑,可以在原代码基础上增加监听器清理逻辑,避免请求结束后监听器仍存活:

const unhandledRejectionHandler = (req, res, next) => {
  const rejectionListener = (reason, p) => {
    self.logger.error("[unhandledRejection] 错误码:%d 信息:%s 调用栈:\n%@", reason.code, reason.message, reason.stack);
    if (res.headersSent) {
      return next();
    }
    const errorMessage = APIHelper.createErrorMessage(500, '', '', reason.message);
    res.status(500).send(errorMessage);
  };

  process.setMaxListeners(process.getMaxListeners() + 1);
  process.once('unhandledRejection', rejectionListener);

  // 请求结束后清理监听器
  const cleanListener = () => {
    process.removeListener('unhandledRejection', rejectionListener);
    process.setMaxListeners(Math.max(process.getMaxListeners() - 1, 0));
  };
  res.on('finish', cleanListener);
  res.on('close', cleanListener);

  next();
};

注意该方案仅为临时补救,仍然存在全局错误关联到错误请求的逻辑问题,建议尽快重构为标准的错误处理方案。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.26 15:06:08