浅谈 JavaScript 错误传递与捕获

166 阅读3分钟

我个人认为在 JavaScript 中错误传递方式有两种:

一种是沿着函数调用栈进行传递,另外一种是沿着 promise 链进行传递

那么与之对应的,错误捕获方式也有两种 try catchpromise.catch

结论

我的习惯还是喜欢先说重点,我们先说结论:

  1. promise 执行回调函数(executor、onFulfilled、onRejected)时发生的同步错误由 promise 自己捕获,并且沿着 promise 链进行传递。
  2. 普通同步错误(非 promise 错误)沿着函数调用栈进行传递,并且都可以被 try catch 捕获。
  3. 无论是 try catch 还是 promise,对于异步错误都无能为力。
  4. 如果想要把 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);
});

参考