你真的會在async/await中捕獲異常嗎?

原文連接:Catching without Awaitingjavascript

當執行一項須要等待一段時間才能返回的任務時,若是使用async/await,就顯得比較麻煩了。若是async方法尚未獲得返回值,咱們就捕獲不到其中的異常。java

在個人上一篇文章Learn to Throw Again中寫到,當使用async/await時,如何同時捕獲到回調函數和throw拋出的錯誤。在這篇文章中,咱們將討論如何在「後臺」中執行異步操做並捕獲異常(這裏使用雙引號,由於在單線程平臺上沒有真正的後臺操做)git

從回調函數的模式開始,思考下列代碼:github

function email(user, message, callback) {
  if (!user) {
    // 拋出異常
    throw new Error('Invlid user');
  }
  if (!user.address) {
    // 回調函數,可能拋出異常
    return callback();
  }
  // 異步的
  return mailer.send(user.address, message, callback);
}
複製代碼

上述代碼遵循典型的throw-on-bad-input / callback-asynchronous-errors模式(一旦程序接收到錯誤的輸入,異步拋出異常),若是咱們想要發出一封郵件,咱們這樣調用:api

email(user, message, () => {});
複製代碼

對於非法的輸入,調用這個函數依舊可能拋出異常。可是,若是電子郵件在傳輸中產生錯誤,這個函數調用時會忽略異步拋出的錯誤。異步

咱們把它改成Promise的版本:async

function email(user, message) {
  if (!user) {
    throw new Error('Invlid user');
  }
  if (!user.address) {
    return Promise.resolve();
  }
  return mailer.send(user.address, message); // 函數返回一個Promise
}
複製代碼

這樣,對於非法的輸入,依舊能夠捕獲到異常。而對於mailer.send()操做則會返回一個Promise,咱們可以輕鬆地經過Promise.catch()捕獲到異常:函數

email(user, message).catch(() => {});
複製代碼

無論是回調函數仍是Promise,他們都是異步的,咱們的應用程序都不會由於email發送而被阻塞。工具

對於async/await的模式,若是在try...catch語句中不使用await關鍵字,那麼try...catch子句不會真正工做。來看下面的async版本:性能

function email(user, message) {
  if (!user) {
    throw new Error('Invlid user');
  }
  if (!user.address) {
    return;
  }
  return mailer.send(user.address, message); // async function
}
複製代碼

若是咱們像這樣去調用:

try {
  email(user, message);
} catch (err) {
  Bounce.rethrow(err, 'system');
}
複製代碼

對於非法的輸入錯誤,仍然會正常地拋出異常,這沒問題。可是對於任何異步返回的異常,例如在mailer.send()拋出的異常,則會被忽略掉。無論這種錯誤咱們想不想捕獲到,反正都是捕獲不到的。爲了修補這個bug,則要使用await關鍵字。可是問題來了,這將會致使整個「後臺操做」的阻塞。

有一種方案是混用async/awaitPromise

email(user, message).catch(() => {});
複製代碼

但這樣的問題在於,對於沒有address的用戶,這個方法返回的返回值類型並非Promise,於是其也不會有catch()方法,所以程序會出現TypeError: Cannot read property ‘catch’ of undefined這樣的錯誤。

你可能會嘗試直接把email()函數聲明爲async函數, 並使得它必定會返回一個Promise,可是這並非一個很好的解決方案,由於async / await其實也只是Promise對象的一層包裝。若是不使用await關鍵字,把一個函數聲明爲async函數是徹底沒有必要的。由於async函數老是要經過返回一個Promise,經過next-tick拿到結果,這樣會浪費Promise包裝和next-tick事件循環機制所形成的性能損耗。

此外,若是要在循環中使用async函數,而且這個循環中執行了不少任務,可是其實不少任務並非真正意義上異步的,那就沒有必要使用async / await,能夠參考hapi.js中的checking if you really need to await下列代碼判斷是否真的須要使用await,這樣或許能得到一些性能的提高:

var response = (typeof func === 'function' ? func(this) : this._invoke(func));
if (response && typeof response.then === 'function') { // Skip await if no reason to
  response = await response;
}
複製代碼

判斷是否真的須要await,其實就是判斷其是否存在then方法,而且then方法是一個函數。由於await的做用其實就是取得一個異步操做的返回結果。

若是你可以保證email方法老是返回一個Promise,咱們能夠經過更改咱們的email()函數來達到這一點,但這樣就顯得急功近利了!代碼顯得十分不簡潔,並且使用了很沒必要要的異步操做。在一個完整的async/await函數調用棧中,不須要咱們手動構建Promise。對於這個例子來講還好,更重要的是,咱們不可能總經過改變email()方法來實現,由於這只是一個例子,在實際運用中,可能email()方法是經過模塊引入的。

其中一種解決方案是經過await關鍵字來調用async函數。一般狀況下,在一個函數中使用阻塞操做,若是不等待這個函數執行完成,它不會拋出異常,可是咱們能夠經過try...catch來包裹:

async function backgroundEmail(user, message) {
  try {
    await email(user, message);
  } catch (err) {
    Bounce.rethrow(err, 'system');
  }
}
複製代碼

而後不經過await調用backgroundEmail

backgroundEmail(user, message);
複製代碼

這樣咱們不但可以捕獲到應用程序的異常,還可以捕獲到異步拋出的異常。

爲了讓異常捕獲更加簡單,咱們使用Bounce模塊,它提供了一個background()方法。

Bounce.background(() => email(user, message));
複製代碼

若是咱們使用Node.jsAssertionError原型,這樣就可以使得Bounce拋出輸入異常的錯誤了。

async/await函數去除了一些同步函數(() => {})的功能,爲了達到和普通函數相同的效果,咱們不得不寫一些額外的代碼來實現。可是使用新的工具庫,能夠很簡便地突破這一限制。

相關文章
相關標籤/搜索