先记住这个答案
Node 提供 throw、strict、warn、warn-with-error-code 和 none。throw 先发出 unhandledRejection,没有对应监听器时升级为未捕获异常;strict 先按未捕获异常处理,若被处理再发出 unhandledRejection。warn 总是发警告;warn-with-error-code 在没有拒绝监听器时警告并把退出码设为 1;none 静默相关警告。Node 从 15 起默认使用 throw。生产应明确采用可观察的失败策略并检查已有监听器,不能把空监听器当作修复;真正可处理的失败仍应在 Promise 链或任务入口捕获。
- throw 与 strict 在存在监听器时仍有差别
- 设置失败退出码不等于立即终止进程
- 入口 ESM 静态加载阶段的拒绝有特殊规则
先建立没有全局监听器的对照实验
下面故意创建未处理拒绝,并安排一个稍后的输出。分别用 node --unhandled-rejections=throw rejection.cjs 等命令运行,观察标准错误、later 是否出现和退出码,三项都要记录。
在没有其他监听器的这个示例中,throw 与 strict 会走未捕获异常路径,warn 和 none 可以继续到 later,warn-with-error-code 也可继续但最终状态为失败。不要只看一个警告字符串就判断进程已经停止。
Promise.reject(new Error('demo-rejection'));
setImmediate(() => console.log('later'));这是测试故障策略的最小输入,应由外层进程以不同模式启动。它不安装监听器,因此可以观察各模式的基础行为;在实际应用中增加监听器后需要重新验证结果。
再加入监听器解释 throw 与 strict
throw 模式下安装 unhandledRejection 监听器会影响是否升级为未捕获异常;如果监听器只是打印一句日志,程序可能继续运行。strict 的升级顺序不同,不能用同一个空监听器假设两种模式结果相同。
若为了实验再安装 uncaughtException 监听器,应明确这是验证事件顺序的隔离夹具,不能作为生产继续运行的方案。第三方监控库也可能安装这些监听,因此最终部署策略需要检查整个运行环境。
把局部错误处理和全局故障策略分开
任务返回的 Promise 应由拥有任务生命周期的调用方等待或显式处理失败。遗漏 await、启动后台 Promise 后丢弃返回值,都会让错误逃出原本准备好的局部 try/catch。
生产可以明确使用 throw 或经过验证的 strict 策略,配合外部重启和告警;选择应基于是否允许监听器接管以及已有工具的行为。none 会减少诊断,不应拿来让故障日志看起来干净;ESM 入口静态加载阶段拒绝还会始终升级为未捕获异常。
容易答错的地方
- 给所有拒绝加一个空全局监听器
- 这改变了部分模式的升级路径,却没有恢复失败任务或解释业务结果。应在任务拥有者处处理错误,全局观察只用于诊断与明确故障策略,不能让遗漏的失败被静默当作成功。
- 认为 warn-with-error-code 会立刻杀进程
- 设置 process.exitCode 与立即退出不同,若还有活动句柄,进程仍可能继续提供服务。需要观察后续回调和资源状态,并为持续运行的服务制定明确失败策略,而不只依赖最终退出码。
面试官还会怎么问?
晚一点补 catch 会发生什么?
未处理判断与事件循环时机有关,之后补处理可能触发 rejectionHandled 等观察,但先前的警告或异常路径已经可能发生。应在创建任务时就建立明确错误所有者,不要依赖稍后补救来控制进程策略。
async 函数里的 throw 会变成拒绝吗?
会,async 函数返回的 Promise 会因 throw 而拒绝。外层只有等待该 Promise 或接上处理链,才能在对应边界处理它;调用函数后立即离开的同步 try/catch 不能捕获未来拒绝。
测试模式时为什么要新建子进程?
模式由进程启动参数控制,测试框架还可能提前注册监听器。独立子进程能隔离事件配置,并让父进程观察标准输出、标准错误和真实退出状态,避免故障测试终止正在执行的整个测试套件。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。