首页 > AI教程 >Node.js错误处理与全局异常捕获详解

Node.js错误处理与全局异常捕获详解

来源:互联网 2026-07-01 06:29:06

Node.js 错误处理与全局异常捕获:别用一个 try/catch 包住整条调用链 业务开发中,错误处理常常被推迟到最后。接口先跑通,catch 留个空壳,日志只写 console.log(err),上线后再补。然而“再补”的时机往往是线上 502 已经出现,你盯着堆栈里毫无上下文的报错发呆。 本

Node.js 错误处理与全局异常捕获:别用一个 try/catch 包住整条调用链

业务开发中,错误处理常常被推迟到最后。接口先跑通,catch 留个空壳,日志只写 console.log(err),上线后再补。然而“再补”的时机往往是线上 502 已经出现,你盯着堆栈里毫无上下文的报错发呆。

Node.js错误处理与全局异常捕获详解

长期稳定更新的攒劲资源: >>>点此立即查看<<<

本文旨在彻底理清 Node.js 中几类错误:同步抛出的异常、Promise 的 rejection、异步回调中的错误——它们走的路径完全不同。接着讨论 Express 5 错误中间件的正确写法,最后分析 uncaughtExceptionunhandledRejection 这两个进程级兜底机制——注意,它们不是用来“续命”的,而是用来体面“退出”的。

环境版本:Node.js 24,Express 5.2.1(npm 上 v5 已是默认安装版本)。

三种错误,三条路径

先理清最容易混淆的部分。下面三段代码看似都“抛了一个错误”,但 Node.js 的处理机制完全不同。

// 1. 同步异常:在当前调用栈上,try/catch 能捕获
function sync() { throw new Error('sync boom') }
try { sync() } catch (err) { // 进得来 }

// 2. Promise rejection:try/catch 无法捕获,需要 .catch 或 await + try/catch
async function asyncFn() { throw new Error('async boom') } // 等价于 return Promise.reject(...)
try { asyncFn() // 未 await,这里什么也抓不到
} catch (err) { // 永远进不来 }

// 3. 异步回调:错误抛在另一个事件循环 tick 里,调用栈已不存在
try {
  setTimeout(() => { throw new Error('callback boom') }, 0)
} catch (err) { // 也进不来 }

第二个例子是新手最容易踩的坑。asyncFn() 返回一个 Promise,如果不 await 或不加 .catch,这个 reject 就变成“无人认领”的状态。try/catch 基于调用栈工作,而 Promise 的 reject 是异步落地的;等到它 reject 时,try 块早已出栈。正确写法只有两种:await asyncFn() 放在 try/catch 内,或者 asyncFn().catch(handler)。混用或遗漏,就会埋下隐患。

第三个例子更隐蔽。setTimeout 的回调在后续的 tick 执行,此时同步的 try 早已结束。这个错误无人能接住,会直接触发进程级的 uncaughtException。Express 官方文档也专门强调了这一点:

“Errors that occur in callbacks that are invoked by the application, rather than the framework, must be passed to next(err).”

因此,关键的分界线在于:调用栈还在时,try/catch 有效;调用栈消失后,必须靠回调中自行捕获,或者用 Promise 链挂住。下一节讲 Express 时会进一步印证。

Express 5 帮你省掉的,和没帮你省的

Express 4 时代,异步路由的错误处理很麻烦。每个 async handler 都需要套一层 asyncHandler 包装,否则 Promise reject 会掉入黑洞,请求直接挂起到超时。社区为此创造了大量 express-async-errorsexpress-async-handler 等库。

Express 5 将此功能收归框架。官方文档原话:

“Express 5 automatically calls next(err) for you when an async route handler rejects.”

也就是说,只要 handler 是 async 函数(返回 Promise),其中 throw 或 reject 时,Express 5 会自动调用 next(err),将错误送入错误中间件。因此 v5 下以下写法是安全的:

import express from 'express'
const app = express()

app.get('/user/:id', async (req, res) => {
  const user = await getUserById(req.params.id)
  if (!user) {
    const err = new Error('user not found')
    err.status = 404
    throw err // 这里 throw,Express 5 自动 next(err)
  }
  res.json(user)
})

文档还补充了一个细节:如果 reject 时未传值(如 Promise.reject()),Express 会使用一个默认的 Error 对象来 next,避免下游因 undefined 而崩溃。

但是——这里有一个 v5 也无力解决的问题,正好对应上一节的第三种错误。Express 只能捕获“handler 返回的那个 Promise”链上的 reject。如果在 handler 中又开启一个异步回调,错误抛在回调里,它和 handler 返回的 Promise 无关,Express 无法感知:

app.get('/', (req, res, next) => {
  setTimeout(() => {
    try { throw new Error('BROKEN') }
    catch (err) { next(err) } // 必须自己接住再 next,少这层 try/catch 错误就漏了
  }, 100)
})

官方文档对这个片段的注释是:

“Errors that occur in callbacks that are invoked by the application, rather than the framework, must be passed to next(err). Otherwise, Express will not catch them.”

所以不要被“Express 5 自动捕获”这句话迷惑。它只捕获 async 边界内的 reject,并非代码中所有异步错误。事件发射器的 error 事件、裸 setTimeoutfs 的回调式 API,这些仍需自行处理。

错误中间件:四个参数,一个不能少

Express 通过函数签名的参数个数来识别错误中间件。普通中间件有三个参数 (req, res, next),错误中间件必须有四个 (err, req, res, next)——少一个,Express 会把它当作普通中间件,错误不会被传进来。

// 注意:即使 next 用不到,也必须写满四个参数,否则 Express 不认
app.use((err, req, res, next) => {
  // 实际项目中应区分“业务错误”和“意外错误”
  const status = err.status || err.statusCode || 500
  if (status >= 500) {
    // 5xx 是我们的责任,带上下文写入日志系统
    req.log?.error({ err, url: req.originalUrl, body: req.body }, 'server error')
  }
  res.status(status).json({
    error: status >= 500  'internal server error' : err.message,
  })
})

几个工程实践要点:

  • 错误中间件应放在所有路由和其他中间件的最后,通过 app.use 注册,这样才能兜底。放在前面的话,后面路由抛的错无法到达它。
  • 不要将内部错误的 message 直接返回给客户端。err.message 可能包含数据库表名、SQL 片段、文件路径。通常只在 4xx 时显示 message(那是给用户看的校验信息),5xx 统一返回固定文案,详细信息写日志。
  • 如果你 next(err) 后没有自定义错误中间件,Express 会使用内置兜底处理器。文档明确说明其行为:res.statusCode 取自 err.statuserr.statusCode,不在 4xx/5xx 范围则设为 500;响应体在生产环境下为状态码对应的 HTML,非生产环境直接输出 err.stack。换句话说,生产环境不应依赖这个默认处理器,因为它会泄露堆栈(只要 NODE_ENV 不是 production)。务必自己编写一个。

进程级兜底:它不是用来续命的

到这一层,讨论的不再是“某个请求出错”,而是“整个进程出错了”。两个关键事件:uncaughtExceptionunhandledRejection

uncaughtException 指的是一个同步异常一路冒泡到事件循环仍未被捕获。Node.js 官方文档描述其默认行为:

“By default, Node.js handles such exceptions by printing the stack trace and exiting with code 1, overriding any previously set process.exitCode.”

即:打印堆栈,以退出码 1 结束进程。一旦注册了 process.on('uncaughtException', …),这个默认的退出行为就被覆盖了——进程不会自动退出。

这正是最危险的地方。许多人这样写:

// 反面教材:千万不要这样做
process.on('uncaughtException', (err) => {
  console.error('caught:', err)
  // 然后……什么也不做,进程继续运行
})

写完感觉良好:异常被我“兜住”了,进程不挂了,稳如磐石。

文档对此态度非常强硬:

“It is not safe to resume normal operation after 'uncaughtException'. The application is in an unknown state.”

还配有一个形象的比喻:

“Attempting to resume normally after an uncaught exception is like yanking the power cord from a computer and then expecting it to continue where it left off.”

一个未捕获的异常意味着程序进入了未定义状态。可能连接池中的连接处于事务一半、某个全局变量修改了一半、某个锁已获取未释放。捂住异常让它继续运行,十次有九次没事,第十次数据就可能损坏。而且损坏位置完全未知,排查成本是直接崩溃的几十倍。

踩过的坑:空 handler 比没有 handler 更糟

之前一个 Node 服务,QPS 不高但很关键。某次发布后内存缓慢增长,大约两三个小时 OOM 一次,被 k8s 重启,重启后继续增长。监控未显示明显请求异常,日志也干干净净。

最终定位到,一段向第三方推送消息的代码使用了 await 但没有任何 catch,而第三方接口偶发超时 reject。这些 reject 变成了 unhandledRejection。当时进程中有段旧代码写了 process.on('unhandledRejection', () => {})——一个空 handler,将所有 reject 全部吞掉。错误消失了,但每个被吞掉的 rejection 关联的 Promise、闭包、请求上下文都无法回收,内存就这样一点点泄漏上去。

教训有两条:第一,空的兜底 handler 比没有 handler 更坏——它把本来该暴露的问题隐藏起来,变成线上的静默失败。第二,真正的修复不在兜底层,而应回到那段 await 加上 catch。兜底是最后一道防线,不是给糟糕代码擦屁股的。

正确用法是什么

uncaughtException 的正确用法,文档说得很清楚:做同步的资源清理,然后退出。

“If you have an HTTP server, you might want to close the server, so that new connections are rejected and existing ones can finish gracefully.”

process.on('uncaughtException', (err, origin) => {
  // 用同步写法写日志,不要用异步——进程马上要结束,异步回调可能执行不到
  fs.writeSync(process.stderr.fd, `uncaught: ${err.stack}\norigin: ${origin}`)
  // 尽量同步地关闭重要资源,然后退出
  process.exit(1)
})

注意是 fs.writeSync,而不是 console.log 再加异步上报。进程即将终止时,await 一个日志上报很可能执行不完。要可靠地落盘,必须使用同步 API。

退出之后由谁来拉起?文档建议使用外部监控:

“It should be restarted by a supervisor or running it in a process manager.”

容器环境下是 k8s 的 liveness probe 加重启策略,传统部署则用 pm2、systemd,或前面讨论过的 cluster 主进程。原则一致:进程崩溃就重启一个干净的,而不是在脏进程中硬撑。

unhandledRejection 及它的演变

unhandledRejection 是 Promise 版本的“无人认领”。文档定义为:

“Emitted whenever a Promise rejection is not handled.”

这里有一个历史包袱值得了解。早期(Node 14 及之前)未处理的 rejection 只打一个 warning,进程继续运行。从 Node 15 开始默认行为改变,--unhandled-rejections 的默认模式变为 throw,文档也明确:未处理的 rejection 如果没有注册 handler,会被当作 uncaught exception 抛出。

“In Node.js 15 and later, the default mode for unhandledRejection is 'throw', which causes the process to exit with a non-zero exit code.”

所以在 Node 24 中,漏掉一个 reject 默认会让进程崩溃。这其实是好事——它迫使开发者把错误处理写完整,而不是让 rejection 悄悄堆积。你需要做的不是注册一个空 handler 去压制它,而是:

process.on('unhandledRejection', (reason) => {
  // 记录错误,然后将其升级为 uncaughtException 的处理路径,统一退出
  fs.writeSync(process.stderr.fd, `unhandledRejection: ${reason}`)
  throw reason instanceof Error  reason : new Error(String(reason))
})

把 rejection 再次抛出,让它走 uncaughtException → 清理 → 退出 → 外部重启这条统一路径。一个进程级错误只用一个出口,千万别让两个 handler 各搞各的。

想监控但不改变退出行为

有时候你只想在崩溃前将错误发送到 Sentry 等平台,但不想接管退出逻辑(接管后就必须自己负责退出,容易出错)。Node 提供了一个专门的事件 uncaughtExceptionMonitor

process.on('uncaughtExceptionMonitor', (err, origin) => {
  MyMonitoringTool.logSync(err, origin)
})

它在 uncaughtException 之前触发,但文档强调它不会改变默认行为:

“This event does not change the default behavior of the process; the process will still exit with a non-zero exit code.”

也就是说,只挂载 monitor、不挂载 uncaughtException,进程该崩溃还是崩溃、该退出还是退出,你只是顺手记录了一笔。这是“我要日志,但退出交给 Node 默认逻辑”的最干净写法。

把这几层串联起来

落地到实际项目中,分层大致如下:

  • 业务代码中,async 函数该 awaitawait,该 catch 就 catch,异步回调中的错误自己 next(err)。这是第一道防线,也是最该用力的——绝大多数错误都应该在这一层被分类处理,根本到不了上层。
  • Express 错误中间件兜住所有路由层漏出来的错误,做统一的状态码映射、脱敏、日志。它处理的是“这个请求失败了”,不影响其他请求。
  • 进程级的 uncaughtException / unhandledRejection 是最后一道,处理的是“这个进程已经不可信了”。它的职责不是续命,而是同步清理 + 退出,重启交给外部监控。

这三层不要混淆。最常见的错误是试图用第三层来补第一层的漏:业务代码懒得写 catch,指望进程级 handler 兜着。结果就是前面提到的内存泄漏——错误确实被“兜”住了,但代价是状态脏了、问题被藏了、最后以更难查的形式爆发。

兜底的价值,恰恰在于它很少被触发。

参考来源

  • Process | Node.js v24 官方文档:uncaughtExceptionunhandledRejectionuncaughtExceptionMonitor 事件行为,以及“It is not safe to resume normal operation after 'uncaughtException'”和拔电源比喻,采集于 2026-06-30
  • Error Handling · Express.js 官方指南:Express 5 自动 next(value)、异步回调需手动 next(err)、错误中间件四参数签名、内置默认错误处理器行为,采集于 2026-06-30
  • Express@5.1.0: Now the Default on npm · Express.js Blog:Express 5 成为 npm 默认安装版本及 v5 版本线说明,采集于 2026-06-30
  • express - npm:确认当前 Express 版本为 5.2.1,采集于 2026-06-30

侠游戏发布此文仅为了传递信息,不代表侠游戏网站认同其观点或证实其描述

热游推荐

更多
湘ICP备14008430号-1 湘公网安备 43070302000280号
All Rights Reserved
本站为非盈利网站,不接受任何广告。本站所有软件,都由网友
上传,如有侵犯你的版权,请发邮件给xiayx666@163.com
抵制不良色情、反动、暴力游戏。注意自我保护,谨防受骗上当。
适度游戏益脑,沉迷游戏伤身。合理安排时间,享受健康生活。