原文連結:Catching without Awaiting
當執行一項需要等待一段時間才能返回的任務時,如果使用async/await
,就顯得比較麻煩了。如果async
方法還沒有得到返回值,我們就捕獲不到其中的異常。
在我的上一篇文章Learn to Throw Again中寫到,當使用async/await
時,如何同時捕獲到回撥函式和throw
丟擲的錯誤。在這篇文章中,我們將討論如何在“後臺”中執行非同步操作並捕獲異常(這裡使用雙引號,因為在單執行緒平臺上沒有真正的後臺操作)
從回撥函式的模式開始,思考下列程式碼:
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
模式(一旦程式接收到錯誤的輸入,非同步丟擲異常),如果我們想要發出一封郵件,我們這樣呼叫:
email(user, message, () => {});
複製程式碼
對於非法的輸入,呼叫這個函式依舊可能丟擲異常。但是,如果電子郵件在傳輸中產生錯誤,這個函式呼叫時會忽略非同步丟擲的錯誤。
我們把它改為Promise
的版本:
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/await
和Promise
:
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.js
的AssertionError
原型,這樣就能夠使得Bounce丟擲輸入異常的錯誤了。
async/await
函式去除了一些同步函式(() => {}
)的功能,為了達到和普通函式相同的效果,我們不得不寫一些額外的程式碼來實現。但是使用新的工具庫,可以很簡便地突破這一限制。