![9ca13c82eb42a00aba5af257ba0008ee.png](https://i-blog.csdnimg.cn/blog_migrate/ef6a901e0e83a71eda2c71c7554e5fad.jpeg)
一、背景
众所周知,由于 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) => {
console.log('Unhandled Rejection at:', p, 'reason:', reason);
});
这是 Node.js 的常识,也是常见的面试题之一,那么这个事件是如何被实现的呢?
二、unhandledRejection 的实现
如果你不想看技术细节的话,读懂下面两句话就够了:
- V8 提供了接口(SetPromiseRejectCallback),当有未捕获的 Promise 错误时,会触发回调。Node.js 会在这个回调中记录下这些错误的 Promise 的信息;
- Node.js 会在每次 Tick 执行完后检查是否有未捕获的错误 Promise,如果有,则触发
unhandledRejection
事件。
如果你想知道具体的代码实现,可以接着向下看……
️三、技术细节
我们以目前 Node.js 最新的 master 分支为例,首先,搜索代码可以找到,unhandledRejection
在这一行(lib/internal/process/promises.js 139)被触发:
function processPromiseRejections() {
// ...
let maybeScheduledTicks = false;
let len = pendingUnhandledRejections.length;
while (len--) {
const promise = pendingUnhandledRejections.shift();
const promiseInfo = 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
事件。
那么就带来了两个问题:
- 是谁调用了这个函数让它触发
unhandledRejection
事件的? - 是谁把有错误的 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<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
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