Node.js内部是如何捕获异步错误的
因为 nodejs 是单线程的,所以一旦发生错误或异常,如果没有及时被处理整个系统就会崩溃。错误异常有两种场景的出现,一种是代码运行中 throw new error 没有被捕获,另一种是 Promise 的失败回调函数,没有对应的 reject 回调函数处理,针对这两种情况 Nodejs 都有默认的统一处理方式,就是给整个进程 process 对象监听相应的错误事件。
背景
众所周知,由于 JavaScript 特殊的 EventLoop 机制,由 Promise 异步产生错误是没有办法使用 try…catch 的:
try { Promise.reject(); } catch(err) { //这里啥都 catch 不到 console.log(err); }
为了解决这个问题,我们必须在每一处产生异步的地方使用.catch()(或者用 async/await 搭配 try…catch):
DoSomethingAsync() .then(...) .catch(...)
但在实际工程里,总是会有一些 Promise 被遗漏掉,没有得到错误处理,在 Node.js 中这就会触发 unhandledRejection 事件,我们可以这样捕获未处理的 Promise 错误:
process.on('unhandledRejection', (reason,p) => { consoloe.log('Unhandled Rejection at:', p, 'reason:', reason); });
这是 Node.js 的常识,也是常见的面试题之一,那么这个事件是如何被实现的呢?
unhandledRejection 的实现
如果你不想看技术细节的话,读懂下面两句话就够了:
1、V8 提供了接口(SetPromiseRejectCallback),当有未捕获的 Promise 错误时,会触发回调。Node.js 会在这个回调中记录下这些错误的 Promise 的信息;
2、Node.js 会在每次 Tick 执行完后检查是否有未捕获的错误 Promise,如果有,则触发 unhandledRejection 事件。
如果你想知道具体的代码实现,可以接着向下看……
技术细节
我们以目前 Node.js 最新的 master 分支为例,首先,搜索代码可以找到,unhandledRejection 在这一行(lib/internal/process/promises.js 139)被触发:
functionprocessPromiseRejections(){ // ... letmaybeScheduledTicks=false; letlen=pendingUnhandledRejections.length; while(len--){ constpromise=pendingUnhandledRejections.shift(); constpromiseInfo=maybeUnhandledPromises.get(promise); if(promiseInfo!==undefined){ promiseInfo.warned=true; const{reason,uid}=promiseInfo; if(!process.emit('unhandledRejection', reason, promise)){ emitWarning(uid,reason); } maybeScheduledTicks=true; } } // ... }
processPromiseRejections() 这个函数在被调用时,会尝试读取 pendingUnhandledRejections 这个数组,然后把里面存着的东西取出来,依次触发 unhandledRejection 事件。
那么就带来了两个问题:
1.是谁调用了这个函数让它触发 unhandledRejection 事件的?
2.是谁把有错误的 Promise 信息放进数组中的?
我们先解决第一个问题,通过搜索代码大法,我们可以找到 processPromiseRejections()这个函数的调用链:
首先,processTicksAndRejections()会在 tock queue 运行到空时,调用 processPromiseRejections():lib/internal/process/task_queues.js 89
function processTicksAndRejections(){ let tock; do{ // 运行 Tock...... }while(!queue.isEmpty()||processPromiseRejections()); // ...... }
然后,processTicksAndRejections()这个函数被设置为每次 Tick 完成后的回调:lib/internal/process/task_queues.js 185
setTickCallback(processTicksAndRejections);
具体设置的方法,在 C++ 层是这里:src/node_task_queue.cc 45-49
static void SetTickCallback(const FunctionCallbackInfo&args) { Environment*env = Environment::GetCurrent(args); CHECK(args[0]->IsFunction()); env->set_tick_callback_function(args[0].As ()); }
也就是说,每次 Tick 完成后,会触发 Tick 的回调,检查是不是有未处理的错误的 Promise,如果有,则会触发 unhandledRejection 事件。
然后是第二个问题,是谁把有错误的 Promise 信息放进数组中的?
同样是搜索代码大法,我们找到了这里:lib/internal/process/promises.js 31-64
function promiseRejectHandler(type,promise,reason){ switch (type) { case kPromiseRejectWithNoHandler:unhandledRejection(promise,reason); break; // ...... } } function unhandledRejection(promise,reason){ //...... pendingUnhandledRejections.push(promise); // ...... }
这段代码里,promiseRejectHandler()识别了传入的 Promise 和 Reject 的类型,如果类型符合,那么会调用 unhandledRejection()向数组中加入这个没有错误处理但是已经报错的 Promise。
那么是谁向 promiseRejectHandler()传入报错的 Promise 的呢?继续找:lib/internal/process/promises.js 148-150
function listenForRejections () { setPromiseRejectCallback(promiseRejectHandler); }
这里把 promiseRejectHandler()设置为每次 Promise Reject 时的回调。
底层实现上,使用了 V8 提供的 SetPromiseRejectCallback()这个接口:src/api/environment.cc 194
void SetIsolateUpForNode(v8::Isolate*isolate){ //...... isolate->SetPromiseRejectCallback(task_queue::PromiseRejectCallback); //...... }
然后在每次 Node.js 启动时,会有一个 setupTaskQueue() 的过程,在这个过程中,PromiseRejectCallback 被设置:lib/internal/process/task_queues.js 180-192
module.exports = { setupTaskQueue () { // Sets the per-isolate promise rejection callback listenForRejections(); //..... } };
知道这些有什么用?
1、第三方实现的 Promise 能触发 unhandledRejection 事件吗?
在上面已经说到,本质上 unhandledRejection 这个事件的实现还是依赖于 V8 实现的 Promise 对象以及对应的接口,也就是说如果我们使用了第三方实现的 Promise,就无法触发这个事件:
const Promise = require('bluebird') Promise.reject() process.on('unhandledRejection', (reason,p) => { // 这里不会被触发,因为 Promise 不是原生实现的 });
2、unhandledRejection 的回调是在何时被执行的?下面这段代码的输出是什么?
Promise.resolve().then(()=>console.log('p1')) Promise.reject() Promise.resolve().then(()=>{ console.log('p2'); process.nextTick(()=>{ console.log('t3') Promise.resolve().then(()=>console.log('p3')) }) }) process.on('unhandledRejection', () => { console.log('unhandledRejection') })
上面我们已经说到,每次 Tick 完成后,会执行并清空 Tock 队列,然后检查有没有异步错误,再触发 unhandledRejection
事件的回调。也就是说 unhandledRejection
的回调是在 Tick 和 Tock 队列都被清空之后进行,所以上面的输出应该是:
p1 p2 t3 p3 unhandledRejection
以上就是 Node.js 内部是如何捕获异步错误的全部内容,NodeJS 的错误处理让人痛苦,在很长的一段时间里,大量的错误被放任不管。但是要想建立一个健壮的 Node.js 程序就必须正确的处理这些错误,而且这并不难学。希望本篇文章能够对大家起到一个参考性的帮助。
码云笔记 » Node.js内部是如何捕获异步错误的