Javascript 使用 promise 進行錯誤處理

2023-02-17 10:53 更新

promise 鏈在錯誤(error)處理中十分強大。當(dāng)一個 promise 被 reject 時,控制權(quán)將移交至最近的 rejection 處理程序。這在實際開發(fā)中非常方便。

例如,下面代碼中所 fetch 的 URL 是錯的(沒有這個網(wǎng)站),.catch 對這個 error 進行了處理:

fetch('https://no-such-server.blabla') // reject
  .then(response => response.json())
  .catch(err => alert(err)) // TypeError: Failed to fetch(這里的文字可能有所不同)

正如你所看到的,.catch 不必是立即的。它可能在一個或多個 .then 之后出現(xiàn)。

或者,可能該網(wǎng)站一切正常,但響應(yīng)不是有效的 JSON。捕獲所有 error 的最簡單的方法是,將 .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 根本不會被觸發(fā)。但是如果上述任意一個 promise rejected(網(wǎng)絡(luò)問題或者無效的 json 或其他),.catch 就會捕獲它。

隱式 try…catch

promise 的執(zhí)行者(executor)和 promise 的處理程序周圍有一個“隱式的 try..catch”。如果發(fā)生異常,它就會被捕獲,并被視為 rejection 進行處理。

例如,下面這段代碼:

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

……與下面這段代碼工作上完全相同:

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

在 executor 周圍的“隱式 try..catch”自動捕獲了 error,并將其變?yōu)?rejected promise。

這不僅僅發(fā)生在 executor 函數(shù)中,同樣也發(fā)生在其處理程序中。如果我們在 .then 處理程序中 throw,這意味著 promise rejected,因此控制權(quán)移交至最近的 error 處理程序。

這是一個例子:

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

對于所有的 error 都會發(fā)生這種情況,而不僅僅是由 throw 語句導(dǎo)致的這些 error。例如,一個編程錯誤:

new Promise((resolve, reject) => {
  resolve("ok");
}).then((result) => {
  blabla(); // 沒有這個函數(shù)
}).catch(alert); // ReferenceError: blabla is not defined

最后的 .catch 不僅會捕獲顯式的 rejection,還會捕獲它上面的處理程序中意外出現(xiàn)的 error。

再次拋出(Rethrowing)

正如我們已經(jīng)注意到的,鏈尾端的 .catch 的表現(xiàn)有點像 try..catch。我們可能有許多個 .then 處理程序,然后在尾端使用一個 .catch 處理上面的所有 error。

在常規(guī)的 try..catch 中,我們可以分析 error,如果我們無法處理它,可以將其再次拋出。對于 promise 來說,這也是可以的。

如果我們在 .catch 中 throw,那么控制權(quán)就會被移交到下一個最近的 error 處理程序。如果我們處理該 error 并正常完成,那么它將繼續(xù)到最近的成功的 .then 處理程序。

在下面這個例子中,.catch 成功處理了 error:

// 執(zhí)行流: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 處理程序就會被調(diào)用。

在下面的例子中,我們可以看到 .catch 的另一種情況。(*) 行的處理程序捕獲了 error,但無法處理它(例如,它只知道如何處理 URIError),所以它將其再次拋出:

// 執(zhí)行流:catch -> catch
new Promise((resolve, reject) => {

  throw new Error("Whoops!");

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

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

    throw error; // 再次拋出此 error 或另外一個 error,執(zhí)行將跳轉(zhuǎn)至下一個 catch
  }

}).then(function() {
  /* 不在這里運行 */
}).catch(error => { // (**)

  alert(`The unknown error has occurred: ${error}`);
  // 不會返回任何內(nèi)容 => 執(zhí)行正常進行

});

執(zhí)行從第一個 .catch (*) 沿著鏈跳轉(zhuǎn)至下一個 (**)

未處理的 rejection

當(dāng)一個 error 沒有被處理會發(fā)生什么?例如,我們忘了在鏈的尾端附加 .catch,像這樣:

new Promise(function() {
  noSuchFunction(); // 這里出現(xiàn) error(沒有這個函數(shù))
})
  .then(() => {
    // 一個或多個成功的 promise 處理程序
  }); // 尾端沒有 .catch!

如果出現(xiàn) error,promise 的狀態(tài)將變?yōu)?“rejected”,然后執(zhí)行應(yīng)該跳轉(zhuǎn)至最近的 rejection 處理程序。但上面這個例子中并沒有這樣的處理程序。因此 error 會“卡住”。沒有代碼來處理它。

在實際開發(fā)中,就像代碼中常規(guī)的未處理的 error 一樣,這意味著某些東西出了問題。

當(dāng)發(fā)生一個常規(guī)的 error 并且未被 try..catch 捕獲時會發(fā)生什么?腳本死了,并在控制臺中留下了一個信息。對于在 promise 中未被處理的 rejection,也會發(fā)生類似的事。

JavaScript 引擎會跟蹤此類 rejection,在這種情況下會生成一個全局的 error。如果你運行上面這個代碼,你可以在控制臺中看到。

在瀏覽器中,我們可以使用 unhandledrejection 事件來捕獲這類 error:

window.addEventListener('unhandledrejection', function(event) {
  // 這個事件對象有兩個特殊的屬性:
  alert(event.promise); // [object Promise] —— 生成該全局 error 的 promise
  alert(event.reason); // Error: Whoops! —— 未處理的 error 對象
});

new Promise(function() {
  throw new Error("Whoops!");
}); // 沒有用來處理 error 的 catch

這個事件是 HTML 標準 的一部分。

如果出現(xiàn)了一個 error,并且在這沒有 .catch,那么 unhandledrejection 處理程序就會被觸發(fā),并獲取具有 error 相關(guān)信息的 event 對象,所以我們就能做一些后續(xù)處理了。

通常此類 error 是無法恢復(fù)的,所以我們最好的解決方案是將問題告知用戶,并且可以將事件報告給服務(wù)器。

在 Node.js 等非瀏覽器環(huán)境中,有其他用于跟蹤未處理的 error 的方法。

總結(jié)

  • ?.catch? 處理 promise 中的各種 error:在 ?reject()? 調(diào)用中的,或者在處理程序中拋出的 error。
  • 如果給定 ?.then? 的第二個參數(shù)(即 error 處理程序),那么 ?.then? 也會以相同的方式捕獲 error。
  • 我們應(yīng)該將 ?.catch? 準確地放到我們想要處理 error,并知道如何處理這些 error 的地方。處理程序應(yīng)該分析 error(可以自定義 error 類來幫助分析)并再次拋出未知的 error(它們可能是編程錯誤)。
  • 如果沒有辦法從 error 中恢復(fù),不使用 ?.catch? 也可以。
  • 在任何情況下我們都應(yīng)該有 ?unhandledrejection? 事件處理程序(用于瀏覽器,以及其他環(huán)境的模擬),以跟蹤未處理的 error 并告知用戶(可能還有我們的服務(wù)器)有關(guān)信息,以使我們的應(yīng)用程序永遠不會“死掉”。

任務(wù)


setTimeout 中的錯誤

你怎么看??.catch? 會被觸發(fā)么?解釋你的答案。

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

解決方案

答案是:不,它不會被觸發(fā)

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

正如本章所講,函數(shù)代碼周圍有個“隱式的 try..catch”。所以,所有同步錯誤都會得到處理。

但是這里的錯誤并不是在 executor 運行時生成的,而是在稍后生成的。因此,promise 無法處理它。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號