2022 年 6 月 18 日

使用 Promise 进行错误处理

Promise 链非常适合进行错误处理。当 Promise 被拒绝时,控制权会跳转到最近的拒绝处理程序。这在实践中非常方便。

例如,在下面的代码中,用于 fetch 的 URL 是错误的(没有这样的网站),并且 .catch 处理了错误

fetch('https://no-such-server.blabla') // rejects
  .then(response => response.json())
  .catch(err => alert(err)) // TypeError: failed to fetch (the text may vary)

正如你所见,.catch 不必紧随其后。它可能出现在一个或多个 .then 之后。

或者,网站可能一切正常,但响应不是有效的 JSON。捕获所有错误的最简单方法是将 .catch 附加到链的末尾

fetch('/article/promise-chaining/user.json')
  .then(response => response.json())
  .then(user => fetch(`https://api.github.com/users/${user.name}`))
  .then(response => response.json())
  .then(githubUser => new Promise((resolve, reject) => {
    let img = document.createElement('img');
    img.src = githubUser.avatar_url;
    img.className = "promise-avatar-example";
    document.body.append(img);

    setTimeout(() => {
      img.remove();
      resolve(githubUser);
    }, 3000);
  }))
  .catch(error => alert(error.message));

通常,这样的 .catch 根本不会触发。但如果上述任何一个 Promise 被拒绝(网络问题或无效的 json 或其他任何问题),那么它就会捕获它。

隐式 try…catch

Promise 执行器和 Promise 处理程序的代码周围有一个“不可见的 try..catch”。如果发生异常,它会被捕获并视为拒绝。

例如,此代码

new Promise((resolve, reject) => {
  throw new Error("Whoops!");
}).catch(alert); // Error: Whoops!

…与以下代码完全相同

new Promise((resolve, reject) => {
  reject(new Error("Whoops!"));
}).catch(alert); // Error: Whoops!

执行器周围的“不可见的 try..catch”会自动捕获错误并将其转换为被拒绝的 Promise。

这不仅发生在执行器函数中,也发生在它的处理程序中。如果我们在 .then 处理程序中 throw,则表示拒绝的 Promise,因此控制权会跳转到最近的错误处理程序。

这里有一个示例

new Promise((resolve, reject) => {
  resolve("ok");
}).then((result) => {
  throw new Error("Whoops!"); // rejects the promise
}).catch(alert); // Error: Whoops!

这适用于所有错误,而不仅仅是 throw 语句导致的错误。例如,编程错误

new Promise((resolve, reject) => {
  resolve("ok");
}).then((result) => {
  blabla(); // no such function
}).catch(alert); // ReferenceError: blabla is not defined

最终的 .catch 不仅会捕获显式拒绝,还会捕获上述处理程序中的意外错误。

重新抛出

正如我们已经注意到的,链末尾的 .catch 类似于 try..catch。我们可以拥有任意多个 .then 处理程序,然后在末尾使用单个 .catch 来处理所有处理程序中的错误。

在常规的 try..catch 中,我们可以分析错误,如果无法处理,则可能重新抛出它。对于 Promise 来说,也可以这样做。

如果我们在 .catchthrow,则控制权将转到下一个最近的错误处理程序。如果我们处理了错误并正常完成,则它将继续到下一个最近的成功的 .then 处理程序。

在下面的示例中,.catch 成功处理了错误

// the execution: catch -> then
new Promise((resolve, reject) => {

  throw new Error("Whoops!");

}).catch(function(error) {

  alert("The error is handled, continue normally");

}).then(() => alert("Next successful handler runs"));

此处,.catch 块正常结束。因此,将调用下一个成功的 .then 处理程序。

在下面的示例中,我们看到了 .catch 的另一种情况。处理程序 (*) 捕获了错误,但无法处理它(例如,它只知道如何处理 URIError),因此再次抛出它

// the execution: catch -> catch
new Promise((resolve, reject) => {

  throw new Error("Whoops!");

}).catch(function(error) { // (*)

  if (error instanceof URIError) {
    // handle it
  } else {
    alert("Can't handle such error");

    throw error; // throwing this or another error jumps to the next catch
  }

}).then(function() {
  /* doesn't run here */
}).catch(error => { // (**)

  alert(`The unknown error has occurred: ${error}`);
  // don't return anything => execution goes the normal way

});

执行从第一个 .catch (*) 跳到链中的下一个 (**)

未处理的拒绝

当错误未处理时会发生什么?例如,我们忘记将 .catch 附加到链的末尾,如下所示

new Promise(function() {
  noSuchFunction(); // Error here (no such function)
})
  .then(() => {
    // successful promise handlers, one or more
  }); // without .catch at the end!

如果发生错误,则 Promise 将被拒绝,并且执行应跳转到最近的拒绝处理程序。但没有这样的处理程序。因此,错误“卡住了”。没有代码可以处理它。

在实践中,就像代码中未处理的常规错误一样,这意味着出现了严重错误。

当发生常规错误且未被 try..catch 捕获时会发生什么?脚本会终止,并在控制台中显示一条消息。未处理的 Promise 拒绝也会发生类似的情况。

JavaScript 引擎会跟踪此类拒绝,并在这种情况下生成全局错误。如果你运行上面的示例,可以在控制台中看到它。

在浏览器中,我们可以使用事件 unhandledrejection 捕获此类错误

window.addEventListener('unhandledrejection', function(event) {
  // the event object has two special properties:
  alert(event.promise); // [object Promise] - the promise that generated the error
  alert(event.reason); // Error: Whoops! - the unhandled error object
});

new Promise(function() {
  throw new Error("Whoops!");
}); // no catch to handle the error

该事件是 HTML 标准 的一部分。

如果发生错误,并且没有 .catch,则 unhandledrejection 处理程序将触发,并获取包含错误信息的 event 对象,以便我们可以采取一些措施。

通常,此类错误是不可恢复的,因此我们最好的办法是向用户告知问题,并可能向服务器报告该事件。

在非浏览器环境(如 Node.js)中,还有其他方法来跟踪未处理的错误。

摘要

  • .catch 处理各种 Promise 中的错误:无论是 reject() 调用,还是在处理程序中抛出的错误。
  • 如果给出了第二个参数(即错误处理程序),则 .then 也以相同的方式捕获错误。
  • 我们应该将 .catch 准确地放在我们希望处理错误并知道如何处理它们的位置。处理程序应分析错误(自定义错误类有所帮助),并重新抛出未知错误(可能是编程错误)。
  • 如果无法从错误中恢复,则不使用 .catch 也是可以的。
  • 无论如何,我们都应该有 unhandledrejection 事件处理程序(对于浏览器,以及其他环境的类似程序)来跟踪未处理的错误,并告知用户(以及我们的服务器),以便我们的应用程序永远不会“突然死亡”。

任务

您认为 .catch 会触发吗?解释您的答案。

new Promise(function(resolve, reject) {
  setTimeout(() => {
    throw new Error("Whoops!");
  }, 1000);
}).catch(alert);

答案是:不会

new Promise(function(resolve, reject) {
  setTimeout(() => {
    throw new Error("Whoops!");
  }, 1000);
}).catch(alert);

正如本章所述,函数代码周围有一个“隐式 try..catch”。因此,所有同步错误都已处理。

但这里的错误不是在执行程序运行时产生的,而是在稍后产生的。因此,promise 无法处理它。

教程地图

评论

评论之前先阅读此内容…
  • 如果您有改进建议,请 提交 GitHub 问题 或提交拉取请求,而不是发表评论。
  • 如果您无法理解文章中的内容,请详细说明。
  • 要插入几行代码,请使用 <code> 标记,要插入多行代码,请使用 <pre> 标记将其包装起来,要插入 10 行以上的代码,请使用沙箱(plnkrjsbincodepen…)