2022 年 6 月 18 日

承諾的錯誤處理

承諾鏈在錯誤處理方面非常棒。當承諾遭到拒絕時,控制權會跳到最近的拒絕處理常式。這在實務上非常方便。

例如,在以下程式碼中,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 根本不會觸發。但如果上述任何承諾遭到拒絕(網路問題或無效的 json 或其他任何問題),它就會捕捉到它。

隱含的 try…catch

承諾執行器和承諾處理常式的程式碼周圍有一個「隱形的 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」會自動捕捉錯誤並將其轉換為拒絕的承諾。

這不僅發生在執行器函式中,也發生在它的處理常式中。如果我們在 .then 處理常式內 throw,表示拒絕的承諾,因此控制權會跳到最近的錯誤處理常式。

以下是範例

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 中,我們可以分析錯誤,如果無法處理,則可能重新擲回錯誤。承諾也可以執行相同的操作。

如果我們在 .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!

如果發生錯誤,承諾會被拒絕,執行應該跳到最近的拒絕處理常式。但沒有拒絕處理常式。因此,錯誤會「卡住」。沒有程式碼可以處理它。

實際上,就像程式碼中未處理的常規錯誤一樣,這表示某處發生了嚴重的問題。

當發生常規錯誤且未被 try..catch 捕捉時,會發生什麼情況?腳本會在主控台中顯示訊息並結束。未處理的承諾拒絕也會發生類似的情況。

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 處理各種承諾中的錯誤:不論是 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」。因此,所有同步錯誤都會被處理。

但這裡的錯誤不是在執行器執行時產生的,而是在稍後產生的。因此,承諾無法處理它。

教學課程地圖

留言

在留言前請先閱讀此內容…
  • 如果您有改進建議,請 提交 GitHub 問題 或提出プル要求,而不是留言。
  • 如果您無法理解文章中的某些內容,請詳細說明。
  • 若要插入幾行程式碼,請使用 <code> 標籤,若要插入多行,請將它們包覆在 <pre> 標籤中,若要插入 10 行以上,請使用沙盒(plnkrjsbincodepen…)