Javascript 使用 promise 進(jìn)行錯(cuò)誤處理

2023-02-17 10:53 更新

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

例如,下面代碼中所 fetch 的 URL 是錯(cuò)的(沒(méi)有這個(gè)網(wǎng)站),.catch 對(duì)這個(gè) error 進(jìn)行了處理:

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

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

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

隱式 try…catch

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

例如,下面這段代碼:

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”自動(dòng)捕獲了 error,并將其變?yōu)?rejected promise。

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

這是一個(gè)例子:

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

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

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

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

再次拋出(Rethrowing)

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

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

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

在下面這個(gè)例子中,.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 塊正常完成。所以下一個(gè)成功的 .then 處理程序就會(huì)被調(diào)用。

在下面的例子中,我們可以看到 .catch 的另一種情況。(*) 行的處理程序捕獲了 error,但無(wú)法處理它(例如,它只知道如何處理 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 或另外一個(gè) error,執(zhí)行將跳轉(zhuǎn)至下一個(gè) catch
  }

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

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

});

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

未處理的 rejection

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

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

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

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

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

JavaScript 引擎會(huì)跟蹤此類 rejection,在這種情況下會(huì)生成一個(gè)全局的 error。如果你運(yùn)行上面這個(gè)代碼,你可以在控制臺(tái)中看到。

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

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

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

這個(gè)事件是 HTML 標(biāo)準(zhǔn) 的一部分。

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

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

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

總結(jié)

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

任務(wù)


setTimeout 中的錯(cuò)誤

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

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

解決方案

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

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

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

但是這里的錯(cuò)誤并不是在 executor 運(yùn)行時(shí)生成的,而是在稍后生成的。因此,promise 無(wú)法處理它。


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

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)