在 Node.js 14 中,async 函数顶部抛出的错误如果没有被外部捕获,进程不会直接崩溃,但后续行为可能变得诡异:请求无响应、定时器不触发、资源泄漏。很多人误以为是死锁或内存泄漏,实际上根源往往是未处理的 Promise rejection。下面从判断、修复到验证,整理一套可操作的排查路径。
修复最直接的方式是在 async 函数内部使用 try-catch 包裹顶部可能出错的代码,并在 catch 块中妥善处理错误。例如,将 `async function foo() { throw new Error('fail'); }` 改为 `async function foo() { try { throw new Error('fail'); } catch (err) { console.error(err); } }`。这样错误会被同步捕获,不会产生未处理的 Promise rejection。如果无法修改函数体,可以在调用处添加 .catch() 或使用 await 并配合 try-catch。
先看日志里有没有 UnhandledPromiseRejectionWarning
如果在 Node.js 14 中,async 函数顶部抛出的错误未被捕获,且没有任何地方 await 或 .catch() 该函数返回的 Promise,那么该错误会触发 unhandledRejection 事件。默认情况下,Node.js 14 仅输出警告信息,不会终止进程,但这可能导致后续代码在错误的 Promise 状态下继续执行,出现资源泄漏或逻辑错乱。判断特征:控制台出现 "UnhandledPromiseRejectionWarning" 日志,同时进程未退出但行为异常,例如请求无响应或定时器不触发。
确认这个特征后,不要急着重启服务。先定位是哪个 async 函数触发的警告——堆栈会指向出错位置。如果没有堆栈,可以用下面 strict 模式强制暴露。
直接修复:try-catch 兜住顶部错误
修复最直接的方式是在 async 函数内部使用 try-catch 包裹顶部可能出错的代码,并在 catch 块中妥善处理错误。例如,将 async function foo() { throw new Error('fail'); } 改为 async function foo() { try { throw new Error('fail'); } catch (err) { console.error(err); } }。这样错误会被同步捕获,不会产生未处理的 Promise rejection。如果无法修改函数体,可以在调用处添加 .catch() 或使用 await 并配合 try-catch。
这种修改适用于你能控制函数定义的场景。如果函数来自第三方库或回调内部,则需要在调用方做 .catch()。注意:在 catch 块中只打印日志可能不够,需要判断是否要恢复操作或回滚状态。
别放过事件监听和 async 函数内部的同步错误
一个常见陷阱是忘记在 async 函数外部使用 await 或 .catch()。例如,在事件监听中使用 emitter.on('event', async () => { throw new Error('err'); });,此时 async 函数返回的 Promise 无人处理,错误被静默丢弃。修复方法是将监听器改为普通函数并内部 try-catch,或使用 emitter.on('event', (err) => ...) 模式。另一个坑是 async 函数内部的同步错误(如 JSON.parse 失败)未被 try-catch 包裹,导致 Promise 进入 rejected 状态但无人问津。
这类问题在代码中容易漏掉,因为监听器或回调不会显式 await。建议在事件处理函数顶部加 try-catch,或改用支持错误处理的库(如 EventEmitter 的错误事件)。验证方式:触发对应事件后观察控制台是否出现 UnhandledPromiseRejectionWarning。
用 strict 模式在开发阶段提前暴露
如果不想逐个修改代码,可以先在开发环境启用严格未处理拒绝模式。在启动命令中添加 --unhandled-rejections=strict,例如 node --unhandled-rejections=strict app.js。此时任何未被捕获的 Promise rejection 都会导致进程直接退出并打印错误堆栈,从而快速定位到出错的 async 函数位置。该模式适合开发环境或测试阶段使用,生产环境建议配合全局监听。
验证:运行后如果进程退出并显示拒绝错误,说明问题存在。修复后再用正常模式启动确认无警告。注意:strict 模式会中断服务,不要在生产长时间运行。
全局监听能兜底,但有隐藏成本
全局监听 unhandledRejection 事件虽然能捕获所有未被处理的 Promise 拒绝,但可能掩盖真正的错误来源。例如,若将监听器写为 process.on('unhandledRejection', (reason) => { console.error(reason); });,则所有未处理的拒绝都只会被记录而不会中断进程,导致异步错误静默积累。应仅在确保日志完整且有能力后续排查时使用,并配合告警机制。更安全的做法是针对每个 async 函数单独处理,避免依赖全局兜底。
如果必须在生产快速止血,可以临时加全局监听并同时上报监控,但之后要逐个修复根因。风险边界:一旦全局监听存在,任何遗漏的拒绝都不会中断进程,可能让 bug 潜伏更久。
总结下来,建议的顺序是:先用 strict 模式定位问题,然后对每个 async 函数使用 try-catch 或 .catch() 处理,事件监听器要额外注意。全局监听只作为最后一层保障,不能替代上游处理。