前两天《记一个 `try…catch` 异步函数的坑》发出后,有同学表示对最后一句不解:
异步函数的调用可能并不在当前栈,也无法被 try…catch 在当前栈捕获到错误。
要解释清楚,一两行是不够的,于是写这篇文章展开解释一下。(这篇文章是基础向。)
函数与栈
首先来看这样一段代码:
function a() {
// ...
}
function b() {
// 代码1
a();
// 代码2
}
function c() {
// 代码3
b()
// 代码4
}
c();
它会怎么执行呢?简单来说是这样:
- 构建一个栈
- 将
c
推入栈,开始执行代码
3 - 将
b
推入栈,开始执行代码1
- 将 a 推入栈,开始执行
a
执行完,出栈- 开始执行
代码 2
b
执行完,出栈- 开始执行
代码 4
c
执行完,出栈
栈是一个先入后出的数据结构,你可以把它当成一个桶,先放进去的东西会被后放进去的东西压在下面,需要先把上层的东西,也就是后放进去的东西拿出来才能拿到先放进去的东西。
发生错误
如果代码执行中发生错误,就会在错误处中断,并逐个出栈。此时我们不仅能看到错误本身,还能看到错误发生处的完整栈信息,对 debug 很有帮助。
try...catch
异步函数出现之前,try...catch
只能捕获当前栈的错误。比如,同样是上面那段代码,稍微改动一下:
// 之前定义 a b c 的代码不变
function d() {
throw new Error('oh my god');
}
try {
c();
} catch (e) {
console.error(e);
}
d();
这段代码里,c()
执行时如果有错误,可以被 try...catch
捕获到;但是 d()
位于另一个栈(或同一个栈的另一个堆叠),就无法捕获。
异步 callback & Promise
Promise
成为规范被纳入标准之前,我们处理异步操作时需要使用回调(callback)。比如侦听事件:
try {
$('.btn').on('click', function (e) {
console.log('hello');
});
} catch (e) {
console.error(e);
}
请注意:这里添加侦听函数的代码,也就是 $().on()
的部分,是在当前栈执行的;而用户点击后执行的操作,即 console.log('hello')
的部分,是在将来某个事件调用栈里执行的。所以,如果后面的函数里有问题,那么如上面所示的代码是无法捕获到错误的。
Promise 也一样。比如下面这段代码:
new Promise((resolve) => {
// 执行完后回调
func0(resolve);
})
.then(() => {
return func1();
})
.then(() => {
return func2();
});
这里的 func0
,func1
,func2
三个函数都是在不同调用栈里执行的,所以如果你在最外面 try...catch
,无法捕获到错误。
这样会给我们编写代码带来一些困难。比如,有时候我们需要同时发起好几个网络请求,有些会成功有些会失败。我们并不知道每个失败请求对应的构造函数是怎么执行的,只能依靠请求内容进行判断,就比较麻烦。
这个问题,在异步函数中得到了解决。
异步函数 async function
这个变化主要是 await
带来的。
如前篇文章所说,在不使用 await
的情况下,try...catch
只会捕获当前调用栈的错误。对于异步函数来说,它在当前栈会返回 Promise 实例,然后就顺利结束,不会抛出任何错误。所以 try...catch
也无法捕获任何错误。
但是添加 await
之后,情况就不同了。这个时候,try...catch
会捕获整个 Promise 里所有的错误,即使函数位于不同调用栈,也可以正确捕获并且显示在一起,方便调试。
欢迎吐槽,共同进步