本文深入解析 Node.js 中的错误处理机制,涵盖 Error 对象结构、throw 抛出、EventEmitter 回调及 process 全局监听。重点剖析 uncaughtException 与 unhandledRejection 的触发原理,结合 V8 源码解读 FatalException 流程,并提供基于 Koa 等框架的最佳实践与日志上报策略。
Node.js 错误处理机制与源码解析:从 Error 对象到 process 监听
前言
近期在项目开发中涉及日志上报功能,借此机会深入研究了如何在 Node.js 环境中完善错误信息的收集与处理机制。以下将详细介绍相关实现细节。
Error 对象结构
01 Error
JavaScript 中的 Error 对象封装了具体的错误信息,包含 name、message 以及错误堆栈 stack 等属性。可以通过 new Error 方式创建实例并抛出,或者调用 Error.captureStackTrace 为已有对象添加 stack 错误堆栈信息后抛出。

错误抛出方式
02 错误抛出几种方式
* Throw*:JavaScript 抛出的异常通过 throw 方法实现,其未必都是 Error 的实例,但由 Node.js 或 JavaScript 运行时发生的错误,通常都是 Error 的实例。
* EventEmitter*:Node.js 形式的错误回调,大部分流(stream)和异步事件都衍生自 EventEmitter 类或其实例,例如 fs、process、stream 等模块。
Process:程序运行过程中抛出的异常,可能由底层库抛出,或是运行中发生的一些 SyntaxError 等错误。
错误捕获方式
03 错误捕获几种方式
try .. catch:这是一种常用的捕获错误方式,在浏览器或 Node.js 环境中均适用。
缺点:仅针对同步异常有效。

*EventEmitter *
由 Events 模块提供的 EventEmitter 类,基于 Observer 模式实现了发布/订阅(publish/subscribe)机制。通过 .on('error', ...)|| .addEventlistener('error', ...) 注册订阅者,.emit() 发布事件,但存在最大的 maxListener 限制,该限制可更改。
此处省略源码展示,实现较为简单,可自行查阅。例如 Koa 的 app 就是基于 EventEmitter 的扩展,因此可以通过监听 error 事件来处理错误。

*Process *
Process 进程对象也是 EventEmitter 的实例,可通过以下两个事件监听 error。
unhandledRejection :Promise 的回调报错,可通过监听该事件进行捕获(catch)。但需注意,由于 Promise 的 rejection 发生时间不确定,实际上可能在 unhandledRejection 事件触发后才 catch 到该信息,这对应着 rejectionHandled 事件的监听,如下所示:

(承上) uncaughtException :其余 JavaScript 运行中发生或抛出的未捕获错误,均可通过监听该事件解决。若不进行该事件的监听,发生异常时,会直接导致程序崩溃(crash)。但不建议用这种方式捕获所有错误,因为程序运行中的错误更应该是被抛出,如果所有 error 都被 catch,程序看似无 bug 实则隐患重重。但在打错误日志时,需要 catch 并上报日志,上报完后,需要继续把 error throw。对于 uncaughtException callback 中抛出的异常不会再捕获,而是以非 0 的状态码 exit。

错误处理小结与最佳实践
04 小结
综上所述,以上关系网可以概括为如图:

个人见解,实际处理中基于以下几点准则:
1、对于需要具象化的错误信息,即我们需要知道具体是哪一块的错误,并且在错误发生时进行个性化处理。这一类型的错误,在程序中执行时要对可能会出错的函数进行 catch,方式有多种: try ... catch(同步);或 通过 Node.js 形式的标准错误回调 (err) => {...}(要求所调用的方法支持该种写法);或 监听 error 事件 .on('error', err => {...})(要求所调用的对象继承自 EventEmitter 实例,并内部发生错误时会 emit('error'))。
2、Promise 形式的错误,可在 Promise 实例的末尾再引入 .catch,可捕获该实例所有 then 中抛出的异常;另外 async 的引入也允许了我们可以如同步操作一样捕获错误。
3、但有时候程序的运行,不可能针对所有的函数操作等都去 try ... catch,也不能保证程序运行一定就无 bug,但必须要能够把这些异常都捕获并上报。因此 process 的两个关键事件: unhandledRejection & uncaughtException,是必须引入的,在程序最外层引入,且越靠前越好。此外针对 unhandledRejection,为防止将不必要的 rejection 上报,可以在收到事件时,先把被 reject 的 promise 和 error 存储起来,设置时间 maxTimeout,超过 maxTimeout 仍未收到 rejectionHandled 的 promise 事件,再上报。
以 Koa 项目为例:

Node.js 源码深度解读
05 源码解读
下图为 Node.js 中对于 process 的初始化等系列流程:

node.cc 其实是 Node.js 运行主要的文件,其中定义了三个重载函数 Start,调用顺序为 3 → 2 → 1,每个函数参数不同处理不同的逻辑;
isolate->AddMessageListener 的监听事件 OnMessage 会在 JavaScript 运行发生错误时触发,是的,只有 error 才会传递;这很好地解释了为什么 process 监听 uncaughtException 就可以监听到所有的抛出的非 Promise 异常;
OnMessage 调用了 FatalException 函数,FatalException 引用 process.fatalException 并传入 error(env 是 env.h 中声明的 Environment 类实例,fatalexceptionstring 也是其中定义的,返回值为 'fatalException');以下为 FatalException 函数的部分内容。

在调用 fatalException 函数前,先调用 v8::TryCatch::setVerbose 把 verbose 设置为 false,则运行时抛出的异常就不会再触发 FatalException;在捕获到运行错误后,把 exitcode 设为 7,并返回;这就解释了为什么在 uncaughtException 时抛出的异常不会再重新触发回调,要知道 EventEmitter 可没帮你做这样的事情。
_fatalException 在触发 "uncaughtException" 事件前其实是会优先检查 domain 是否存在,当 domain 不存在时才会调用 uncaughtException 的,但 domain 这个已经被废除了,也就不累述了。
unhandledRejection 事件的触发整体思路也差不多,首先会调用 SetupPromises 初始化,然后调用 v8::Isolate::SetPromiseRejectCallback 进行监听 ... 并且跟 uncaughtException 不同的是 unhandledRejection 的触发只会打印 warning 并不会把整个程序给 crash 了。
06 END
参考网站: https://v8.paulfryzel.com/docs/master/index.html
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对本站的支持。