我个人认为在 JavaScript 中错误传递方式有两种:
一种是沿着函数调用栈进行传递,另外一种是沿着 promise 链进行传递。
那么与之对应的,错误捕获方式也有两种 try catch 与 promise.catch。
结论
我的习惯还是喜欢先说重点,我们先说结论:
- promise 执行回调函数(executor、onFulfilled、onRejected)时发生的同步错误由 promise 自己捕获,并且沿着 promise 链进行传递。
- 普通同步错误(非 promise 错误)沿着函数调用栈进行传递,并且都可以被 try catch 捕获。
- 无论是 try catch 还是 promise,对于异步错误都无能为力。
- 如果想要把 promise 错误传递到 try catch 中,需要 await。
解释
我们先从一段代码说起,先看下面这段代码:
class MyPromise {
constructor(executor) {
executor();
}
}
try {
new MyPromise((resolve, reject) => {
throw new Error("error");
});
} catch (error) {
console.log("try catch error", error);
}
上面的 try catch 很自然地捕获了错误,我们接着再看:
try {
new Promise((resolve, reject) => {
throw new Error("error");
}).catch((error) => {
console.log("promise error", error);
});
} catch (error) {
console.log("try catch error", error);
}
这次我们的 try catch 不再能捕获错误了,错误被 promise 捕获,并传递给了 promise.catch。
那么如果我们的 try catch 想要捕获错误该怎么办呢?其实很简单,添加一个 await 即可,如下:
const fn = async () => {
try {
await new Promise((resolve, reject) => {
throw new Error("error");
});
} catch (error) {
console.log("try catch error", error);
}
};
fn();
接下来我们看看下面这段代码,try catch 是否能够捕获到错误呢?
const fn = async () => {
throw new Error("error");
};
try {
fn();
} catch (error) {
console.log("try catch error", error);
}
运行以下就知道,其实是不能的。其实上面这段代码,就等同于下面这段:
try {
new Promise(() => {
throw new Error("error");
});
} catch (error) {
console.log("try catch error", error);
}
因为 async 标记函数,返回值都是一个 promise。而 async 函数中的每一段 await,可以理解成 promise 的一个 executor。
最后我们再简单看看异步的情况,如下:
try {
setTimeout(() => {
throw new Error("error");
});
} catch (error) {
console.log("try catch error", error);
}
new Promise((resolve, reject) => {
setTimeout(() => {
throw new Error("error");
});
}).catch((error) => {
console.log("promise error", error);
});
很显然,上面的两种情况,错误都是无法捕获的。
扩展
全局捕获
既然错误的传递方式有两种,进而错误捕获方式也有两种。而他们的全局错误捕获方式自然也有对应的两种,如下:
window 环境:error、unhandledrejection node 环境:uncaughtException、unhandledRejection
await 原理
我们知道 await 是 promise 错误传递给 try catch 的桥梁,那么我们来看看 ECMAScript 规范对于它的实现,具体是如何规定的。看完之后发现还是黄玄大佬总结得好,一句话概括:太长不看:因为 await 会把 rejected promise 转变成了一个 throw……就这么简单。
有兴趣的朋友可以自己看一下规范。
Promise.try
Promise.try 作为一个新出的 API,其实也无法捕获异步错误。本质上可以理解成:只是 await 了一下我们传入的回调函数,再使用 try catch 捕获了一下错误,仅此而已。
举个例子:
new Promise(async (resolve, reject) => {
throw new Error("error");
}).catch((error) => {
console.log("promise error", error);
});
上面的代码,promise.catch 是无法捕获到错误,可以理解成没有 await executor。
而 Promise.try 就不会,如下:
Promise.try(async () => {
throw new Error("error");
}).catch((error) => {
console.log("promise error", error);
});
而对于真正的异步错误,Promise.try 也是无能为力的,比如:
Promise.try(() => {
setTimeout(() => {
throw new Error("error");
});
}).catch((error) => {
console.log("promise error", error);
});