Node.js 项目真正难维护的地方,往往不是成功路径,而是失败路径。同步异常、Promise 拒绝、流错误、进程级异常看起来都属于“报错”,但它们所在的层次和处理方式并不一样。这一篇会把错误边界真正分层说明白。
为什么不能把所有错误都当成一类?
很多初学者会把错误简单理解成“出了问题就 try catch 一下”,但在 Node.js 里,错误来源非常多:
- 同步函数内部直接抛出的异常;
- Promise 链中的拒绝;
- 回调风格 API 通过参数传回的错误;
- 事件驱动对象触发的
error事件; - 进程级别未处理的异常。
如果不把这些层次分清楚,你就会很容易出现两种极端:要么到处胡乱兜底,什么都 catch;要么错误四处乱飞,最终变成难以定位的线上故障。
try / catch 的边界到底在哪里?
try / catch 很重要,但它只对当前同步调用栈里的异常有效。也就是说,如果一个错误已经脱离了当前同步执行边界,单纯在外面包一层 try / catch 并不能自动接住它。
这也是为什么很多人第一次写异步代码时会困惑:明明我包了 try / catch,为什么错误还是没被抓到?原因不在于 Node 不讲理,而在于错误已经发生在另一个异步边界里了。
所以,一个很基本的认识是:try / catch 擅长处理同步调用链中的异常,以及 await 当前 Promise 时重新抛回来的异常,但它不是“所有错误的万能网”。
Promise 拒绝和异步错误应该怎么理解?
在现代 Node.js 里,大量异步逻辑都建立在 Promise 上。这时错误的关键不只是“有没有抛出”,而是:
- 拒绝是否被显式处理;
- 拒绝是否在合适的层次被转换;
- 调用链最终能否感知失败。
如果一个 Promise 拒绝了,却没有被任何地方接住,那么它最终可能演变成更高层面的未处理拒绝问题。这也是为什么成熟项目里通常都会有很明确的错误传播约定:局部能处理的局部处理,处理不了的就及时向上抛,而不是静默吞掉。
真正危险的通常不是报错本身,而是“看起来程序继续运行了,其实状态已经不可信”。
流、事件和回调里的错误为什么容易漏?
Node.js 里有很多错误并不是以 throw 的形式直接出现的。比如:
- 传统回调 API 会把错误作为第一个参数传回来;
- 某些流和
EventEmitter对象会通过error事件暴露失败; - 某些组合型流程中,错误可能藏在链路某一段里。
这意味着你不能只盯着 throw 和 catch。有些错误的传播协议,本来就不走普通异常机制。也正因为如此,理解不同抽象各自如何报告错误,是 Node.js 稳定性的关键组成部分。
全局错误边界能不能兜底一切?
Node.js 提供了像 uncaughtException、unhandledRejection 这样的全局边界能力,但它们更适合被理解成“最后的告警和收尾入口”,而不是常规业务错误处理方案。
原因很简单:一旦错误已经跑到进程级别,往往说明某条边界之前就已经处理失败了。此时你也许还能记录日志、做清理、上报问题,但很难继续假定当前进程状态完全健康。
所以,一个更成熟的态度通常是:
- 不把全局异常监听当作日常业务分支;
- 把它当作最后的保护网和诊断出口;
- 明白很多进程级错误发生后,重启或退出往往比强行继续更安全。
总结
这一篇我们把 Node.js 的错误世界拆成了几个层次:同步异常、Promise 拒绝、回调与事件错误,以及更高层的进程级异常边界。
真正值得记住的,不是哪一个 API,而是一个原则:错误应该在合适的边界被明确处理。局部能解决的局部解决,解决不了的向上抛;该暴露的暴露,该停止的停止,而不是默默吞掉。
下一篇我们会把视角切到网络层,从 Node.js 的原生网络能力开始理解服务端通信。