web前端开发技术博客
当前位置: 前端技术 > Node.js内部是如何捕获异步错误的

Node.js内部是如何捕获异步错误的

2019-05-10 分类:前端技术 作者:码云 阅读(3874)

因为nodejs是单线程的,所以一旦发生错误或异常,如果没有及时被处理整个系统就会崩溃。错误异常有两种场景的出现,一种是代码运行中throw new error没有被捕获,另一种是Promise的失败回调函数,没有对应的reject回调函数处理,针对这两种情况Nodejs都有默认的统一处理方式,就是给整个进程process对象监听相应的错误事件。

Node.js内部是如何捕获异步错误的

背景

众所周知,由于JavaScript特殊的EventLoop机制,由Promise异步产生错误是没有办法使用try…catch的:

1
2
3
4
5
6
try {
    Promise.reject();
} catch(err) {
    //这里啥都catch不到
    console.log(err);
}

为了解决这个问题,我们必须在每一处产生异步的地方使用.catch()(或者用async/await搭配try…catch):

1
2
3
DoSomethingAsync()
    .then(...)
    .catch(...)

但在实际工程里,总是会有一些Promise被遗漏掉,没有得到错误处理,在Node.js中这就会触发unhandledRejection事件,我们可以这样捕获未处理的Promise错误:

1
2
3
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)被触发:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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

1
2
3
4
5
6
7
function processTicksAndRejections(){
    let tock;
    do{
        // 运行Tock......
    }while(!queue.isEmpty()||processPromiseRejections());
        // ......
}

然后,processTicksAndRejections()这个函数被设置为每次Tick完成后的回调:lib/internal/process/task_queues.js 185

1
setTickCallback(processTicksAndRejections);

具体设置的方法,在 C++ 层是这里:src/node_task_queue.cc 45-49

1
2
3
4
5
static void SetTickCallback(const FunctionCallbackInfo<Value>&args) {
    Environment*env = Environment::GetCurrent(args);
    CHECK(args[0]->IsFunction());
    env->set_tick_callback_function(args[0].As<Function>());
}

也就是说,每次Tick完成后,会触发Tick的回调,检查是不是有未处理的错误的Promise,如果有,则会触发unhandledRejection事件。

然后是第二个问题,是谁把有错误的Promise信息放进数组中的?

同样是搜索代码大法,我们找到了这里:lib/internal/process/promises.js 31-64

1
2
3
4
5
6
7
8
9
10
11
12
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

1
2
3
function listenForRejections () {
    setPromiseRejectCallback(promiseRejectHandler);
}

这里把promiseRejectHandler()设置为每次Promise Reject时的回调。

底层实现上,使用了V8提供的SetPromiseRejectCallback()这个接口:src/api/environment.cc 194

1
2
3
4
5
void SetIsolateUpForNode(v8::Isolate*isolate){
    //......
    isolate-&gt;SetPromiseRejectCallback(task_queue::PromiseRejectCallback);
    //......
}

然后在每次 Node.js 启动时,会有一个 setupTaskQueue() 的过程,在这个过程中,PromiseRejectCallback 被设置:lib/internal/process/task_queues.js 180-192

1
2
3
4
5
6
7
module.exports = {
    setupTaskQueue () {
    // Sets the per-isolate promise rejection callback
        listenForRejections();
    //.....
    }
};

知道这些有什么用?

1、第三方实现的Promise能触发unhandledRejection事件吗?

在上面已经说到,本质上unhandledRejection这个事件的实现还是依赖于V8实现的Promise对象以及对应的接口,也就是说如果我们使用了第三方实现的Promise,就无法触发这个事件:

1
2
3
4
5
const Promise = require('bluebird')
Promise.reject()
process.on('unhandledRejection', (reason,p) => {
    // 这里不会被触发,因为 Promise 不是原生实现的
});

2、unhandledRejection的回调是在何时被执行的?下面这段代码的输出是什么?

1
2
3
4
5
6
7
8
9
10
11
12
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 队列,然后检查有没有异步错误,再触发 

1
unhandledRejection
 事件的回调。也就是说 
1
unhandledRejection
 的回调是在 Tick 和 Tock 队列都被清空之后进行,所以上面的输出应该是:

1
2
3
4
5
p1
p2
t3
p3
unhandledRejection

以上就是Node.js内部是如何捕获异步错误的全部内容,NodeJS的错误处理让人痛苦,在很长的一段时间里,大量的错误被放任不管。但是要想建立一个健壮的Node.js程序就必须正确的处理这些错误,而且这并不难学。希望本篇文章能够对大家起到一个参考性的帮助。

「如果觉得我的文章对您有用,请帮助本站成长」

赞(9) 打赏

觉得文章有用就打赏一下文章作者

支付宝
微信
9

觉得文章有用就打赏一下文章作者

支付宝
微信

上一篇:

下一篇:

共有 0 条评论 - Node.js内部是如何捕获异步错误的

博客简介

码云笔记: mybj123.com,一个关注Web前端开发技术的博客,主要记录和总结前端工作中常用的知识及我的生活。
更多博客详情请看关于博客

圈子

关注微信公众号
关注微信公众号

精彩评论

服务热线:
 13888888888

 QQ在线交流