- 文档
- 教程
- 后端
【免费下载链接】nodebestpractices
✅ The Node.js best practices list (July 2026)
本文是 nodebestpractices 仓库「错误处理最佳实践」系列中的核心一篇。现代 Node.js/Express 应用的绝大多数业务逻辑运行在 Promise 链(
.then处理器、函数回调或catch块)中,一旦某处遗漏.catch,错误既不会被uncaughtException事件捕获,也不会被任何请求级错误中间件处理,而是直接"静默消失"。本文将带你还原错误消失的场景、理解 Node 内置的unhandledRejection事件,并结合仓库中集中式错误处理、进程优雅退出等配套最佳实践,搭建一套"开发者纪律 + 机制兜底"双保险的异步错误防线。读完后你将掌握:如何用process.on('unhandledRejection')拦截所有漏网 Promise 错误、如何把它与集中式错误处理器串联,以及什么情况下应该让进程退出重启。
为什么未处理的 Promise 拒绝会成为"静默杀手"
核心问题:uncaughtException管不到 Promise 内部
多数开发者都知道要为"未捕获异常"注册兜底处理器,于是写了:
process.on('uncaughtException', (error) => { // ... });但一个常被忽略的事实是:在 Promise 链内部抛出的错误,根本不会触发uncaughtException事件。只要开发者忘记在某个.then链的末尾追加.catch,这些位置产生的错误就不会被任何错误处理器接收,悄无声息地消失。
这正是仓库中 README.md 2.10 小节 所强调的 TL;DR:
任何在 Promise 内抛出的异常都会被吞掉并丢弃,除非开发者明确记得去处理它——即使你的代码已经订阅了
process.uncaughtException也无济于事!解决办法是注册process.unhandledRejection事件。
值得说明的是,较新版本的 Node.js 在出现未处理拒绝时会在控制台打印一条警告信息(unhandled rejection warning)。这虽然能帮助开发者"注意到出了事",但正如原文档所指出的:这显然不是一种正确的错误处理方式——生产环境中,你需要的不是一条随进程生命周期一闪而过的警告,而是一套确定性的、可记录、可决策的处理机制。
错误的"消失"现场还原
原文档给出了一个非常直观的例子:
DAL.getUserById(1).then((johnSnow) => { // 这个错误会就这样凭空消失 if (johnSnow.isAlive === false) throw new Error('ahhhh'); });throw new Error('ahhhh')发生在.then回调内部,而这条 Promise 链没有.catch。结果就是:既没有uncaughtException兜底,也没有任何中间件能感知,错误被静默吞掉,日志里什么都查不到。
这种"有错误但毫无痕迹"的情况在排查线上问题时极其致命:用户操作失败了、数据没写入,但你的监控、日志全都风平浪静。
兜底方案:订阅process.on('unhandledRejection')
最简单的解决思路:永远记得加.catch
最直接的做法当然是约束团队纪律:在每个 Promise 链的末尾都补上.catch,并把错误统一转发给集中式错误处理器。然而,原文档明确指出,仅靠开发者的自律来构建错误处理策略是脆弱的(fragile)——人总会犯错,"如果一个人可能犯错,那么他迟早会犯"。
因此,强烈推荐的做法是:在自律之外,再加一道优雅的机制兜底(graceful fallback),订阅process.on('unhandledRejection', callback)。这样任何没有被局部处理的 Promise 错误,最终都会被这道防线接住。
JavaScript 实现
process.on('unhandledRejection', (reason, p) => { // 我刚刚捕获到一个未处理的 promise rejection, // 由于下面已经有针对未处理错误的兜底处理器, // 直接把它抛出去,交给那个处理器统一处理 throw reason; }); process.on('uncaughtException', (error) => { // 我刚刚接收到一个从未被处理过的错误, // 是时候处理它,然后决定是否需要重启进程 errorManagement.handler.handleError(error); if (!errorManagement.handler.isTrustedError(error)) process.exit(1); });这里的关键设计在于:
unhandledRejection回调拿到reason(拒绝原因,通常就是 Error 对象)后主动throw reason——这一步会把 Promise 拒绝转化为同步异常,从而进入uncaughtException事件,让所有未处理错误走同一条处理通道;uncaughtException回调不再自行发散处理,而是把错误交给集中式错误处理器errorManagement.handler.handleError(error);- 处理完(记录日志、发送监控指标等)之后,通过
isTrustedError(error)判断该错误是否为可信的操作性错误(operational error),若不是,则process.exit(1)让进程退出,等待 PM2、Forever 等进程守护工具以干净状态重启。
TypeScript 实现
process.on('unhandledRejection', (reason: string, p: Promise<any>) => { // 我刚刚捕获到一个未处理的 promise rejection, // 由于下面已经有针对未处理错误的兜底处理器, // 直接把它抛出去,交给那个处理器统一处理 throw reason; }); process.on('uncaughtException', (error: Error) => { // 我刚刚接收到一个从未被处理过的错误, // 是时候处理它,然后决定是否需要重启进程 errorManagement.handler.handleError(error); if (!errorManagement.handler.isTrustedError(error)) process.exit(1); });与集中式错误处理器的协同:让所有错误走同一条通道
上面的示例中反复出现errorManagement.handler这个对象,这正是仓库 Handle errors centrally. Not within middlewares(集中处理错误,而非在中间件内处理) 所倡导的架构。它的核心观点是:必须存在一个专用的错误处理对象,统一负责"让错误可见"(写入格式化日志、上报监控指标)以及"决定进程是否崩溃",而不能把处理逻辑散落在 Express 错误中间件、定时任务、消息队列订阅者等各处。
一个典型的错误流如下:
某个模块抛出错误 → API 路由捕获错误 → 错误传播到错误捕获中间件(或其它请求级错误捕获机制) → 调用集中式错误处理器在该文档中,集中式错误处理器的最小实现长这样:
module.exports.handler = new errorHandler(); function errorHandler() { this.handleError = async (error, responseStream) => { await logger.logError(error); await fireMonitoringMetric(error); await crashIfUntrustedErrorOrSendResponse(error, responseStream); }; }TypeScript 版本与之对应:
class ErrorHandler { public async handleError(error: Error, responseStream: Response): Promise<void> { await logger.logError(error); await fireMonitoringMetric(error); await crashIfUntrustedErrorOrSendResponse(error, responseStream); }; } export const handler = new ErrorHandler();而在 中央化处理文档的代码示例 中,unhandledRejection事件同样被显式纳入这套统一通道:
process.on("unhandledRejection", (reason) => { errorHandler.handleError(reason); });也就是说,仓库推荐的最小闭环是:unhandledRejection兜底捕获 → 转发给集中式错误处理器 → 处理器负责日志、监控与崩溃决策。你完全可以在上面给出的"throw reason 方案"和"直接转交 handleError 方案"之间按团队习惯选择,二者本质都是在把散落的 Promise 错误收敛到单一处理点。
仓库中对应的错误处理整体架构与参与者流程可以用下图概括(来自 centralizedhandling.md):
兜底之后的决策:可信错误与进程退出
接住了错误只是第一步,接下来要回答"该不该让进程死掉"这个问题。这依赖isTrustedError的判断,而它的依据来自 Distinguish operational vs programmer errors(区分操作性错误与程序员错误) 中定义的isOperational标记:
// 把错误对象标记为操作性(可信)错误 const myError = new Error('How can I add new product when no value provided?'); myError.isOperational = true;- 操作性错误(operational error):你清楚发生了什么及影响范围,例如某个 HTTP 服务因连接问题查询失败。这类错误通常记入日志即可,进程无需重启。
- 程序员错误(programmer error):你完全不知道为什么会发生、错误从哪里来,例如读取了 undefined 的值、连接池泄漏内存。此时应用可能已处于不一致状态,最稳妥的做法是优雅重启。
对应地,Exit the process gracefully when a stranger comes to town(陌生错误到来时优雅退出进程) 给出了与本文示例完全一致的决策逻辑:
process.on('uncaughtException', (error) => { errorManagement.handler.handleError(error); if(!errorManagement.handler.isTrustedError(error)) process.exit(1) }); function errorHandler() { this.handleError = (error) => { return logger.logError(error) .then(sendMailToAdminIfCritical) .then(saveInOpsQueueIfCritical) .then(determineIfOperationalError); } this.isTrustedError = (error) => { return error.isOperational; } }其中isTrustedError的语义就是"该错误是否被标记为可信任的操作性错误"(error.isOperational === true)。如果错误不可信(例如属于程序员错误),就调用process.exit(1)退出进程,交由 PM2 / Forever 等进程守护工具以干净状态重新拉起,避免应用带着损坏状态继续服务、让后续所有请求连锁失败。
Node.js 官方文档也给出了相同的立场:由于throw的机制特性,几乎没有可能"安全地从断点继续",最稳妥的响应方式就是关闭进程;对于 Web 服务器而言,更好的做法是向触发错误的请求返回错误响应、让其他请求正常完成、并在该 worker 中停止接收新请求。
小测验:哪些 Promise 错误真的会打印出来?
原文档引用了一篇来自 James Nelson 博客的经典测试题,用来检验你对"错误到底会不会显现"的直觉:
Promise.resolve('promised value').then(() => { throw new Error('error'); }) Promise.reject('error value').catch(() => { throw new Error('error') }); new Promise((resolve, reject) => { throw new Error('error'); });直觉上,你可能认为这三段代码都会在控制台打印出错误。但现实是:相当多的现代 JavaScript 环境对这三段代码一条错误都不会打印。作者由此给出的结论,也是本文兜底策略的哲学根基:
人的问题在于,如果一个人可能犯错误,那么在某个时刻他一定会犯。既然如此,我们就应该把系统设计成"错误造成的伤害尽可能小",这意味着默认就处理错误,而不是丢弃它们。
从源头减少漏网之鱼:让异步错误更容易被捕获
兜底机制解决的是"最后一道防线",但更优的做法是从源头降低出错概率。Use Async-Await or promises for async error handling(使用 async/await 或 Promise 处理异步错误) 给出了推荐写法:
Promise 链式风格:
return functionA() .then(functionB) .then(functionC) .then(functionD) .catch((err) => logger.error(err)) .then(alwaysExecuteThisFunction)async/await 风格(推荐,更接近同步思维):
async function executeAsyncTask () { try { const valueA = await functionA(); const valueB = await functionB(valueA); const valueC = await functionC(valueB); return await functionD(valueC); } catch (err) { logger.error(err); } finally { await alwaysExecuteThisFunction(); } }回调风格的错误处理则被明确列为反模式:它迫使你在每一层回调里重复检查err !== null,代码层层嵌套、难以推理,也更容易出现"漏检查"导致错误被吞。Promise 与 async/await 通过return/throw控制程序流,支持开发者熟悉的try-catch风格,把主代码路径从"每个函数都要处理错误"中解放出来——这本身就大幅降低了产生未处理拒绝的概率。
实践清单
- 纪律层:所有 Promise 链以
.catch收尾,异步函数用try/catch/finally包裹,错误统一转发给集中式处理器; - 机制层:注册
process.on('unhandledRejection', (reason) => { throw reason; })(或直接errorHandler.handleError(reason)),确保任何漏网错误都有归宿; - 决策层:在
uncaughtException处理器中调用集中式handleError,并通过isTrustedError/error.isOperational决定是仅记日志还是process.exit(1)触发守护进程重启; - 风格层:优先 async/await + try-catch 的写法,减少回调嵌套,从源头降低遗漏
.catch的概率。
上述代码片段、架构决策与设计理由,均可在仓库 sections/errorhandling 目录下找到原始文档(英文版见 catchunhandledpromiserejection.md,本主题在 README.md 的 2.10 小节 有 TL;DR 摘要),配套的 集中式处理、优雅退出进程、区分操作性错误与程序员错误 以及 异步错误处理 几篇可以连读,构成一套完整的 Node.js 错误处理方案。
- 文档
- 教程
- 后端
【免费下载链接】nodebestpractices
✅ The Node.js best practices list (July 2026)
相关推荐
Node.js 未处理 Promise 拒绝(unhandledRejection)捕获最佳实践:从 `.catch` 遗漏到进程级兜底
Node.js 未处理 Promise 拒绝(unhandledRejection)捕获最佳实践:从 .catch 遗漏到进程级兜底 本指南聚焦于 Node.j
文档教程后端Node.js 最佳实践:捕获未被处理的 Promise 拒绝(unhandledRejection)
Node.js 最佳实践:捕获未被处理的 Promise 拒绝(unhandledRejection) 导读 在 Node.js/Express 应用中,绝大多
文档教程后端vscode-leetcode异步错误处理:捕获Promise异常
vscode leetcode异步错误处理:捕获Promise异常 在VSCode中解决LeetCode问题时,异步操作无处不在,从获取题目列表到提交代码,都依
开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考