如何在Javascript中優雅的使用Async和Await進行錯誤的處理?
ES7的出現,允許我們使用Async和Await進行編寫非同步函式,使用這種寫法我們的非同步函式看起來就跟同步程式碼一樣。在之前的版本,我們引入了Promise寫法,來簡化我們非同步程式設計的流程,同時也避免了回撥地獄。
回撥地獄是語義化產生的一個術語,他的釋義可以用下面這種情況進行闡述:
function AsyncTask() {
asyncFuncA(function(err, resultA){
if(err) return cb(err);
asyncFuncB(function(err, resultB){
if(err) return cb(err);
asyncFuncC(function(err, resultC){
if(err) return cb(err);
// And so it goes....
});
});
});
}
不斷的回撥,使得程式碼維護和管理控制流程變得十分的困難。我們不妨考慮下這種情況,假如某個if語句需要執行其他的方法,而回調函式FunctionA的結果為foo。
使用Promise來改善這種情況
ES6和Promise的出現,我們得以簡化之前夢魘般的回撥程式碼如下:
function asyncTask(cb) {
asyncFuncA.then(AsyncFuncB)
.then(AsyncFuncC)
.then(AsyncFuncD)
.then(data => cb(null, data)
.catch(err => cb(err));
}
這樣編寫是不是看起來更加棒了?
但是在真實場景中,非同步流可能會變得更加複雜一些。舉例來說,
假如在你的一個(node.js)伺服器中,你可能想要將一個數據1儲存到資料庫中,(步驟1)
然後根據儲存的資料1查詢另外一個數據2,(步驟2)
如果查詢到了資料2,執行其他的一些非同步任務,(其他任務),
等到所有的任務全部執行完成之後,你可能需要使用你在步驟1中得到的結果用來反饋給使用者。
並且假如在執行任務的過程中發生了錯誤,你想要告訴使用者在哪個步驟發生了錯誤。
在使用了Promise語法後,這樣當然看起來更加的簡潔了,但是,在我看來仍然有一點混亂。
ES7 Async/await
注意:您需要使用轉譯器才能使用Async/Await,您可以使用babel外掛或Typescript來新增所需的工具。
這是我發現的 async/await 確實有用,它允許我們像下面一樣編寫程式碼:
async function asyncTask(cb) {
const user = await UserModel.findById(1);
if(!user) return cb('No user found');
const savedTask = await TaskModel({userId: user.id, name: 'Demo Task'});
if(user.notificationsEnabled) {
await NotificationService.sendNotification(user.id, 'Task Created');
}
if(savedTask.assignedUser.id !== user.id) {
await NotificationService.sendNotification(savedTask.assignedUser.id, 'Task was created for you');
}
cb(null, savedTask);
}
上面的程式碼看起來更加的乾淨了,但是如何處理錯誤報錯呢?
在執行非同步任務使用Promise的時候可能會發生一些錯誤類似資料庫連接出錯,資料庫模型驗證錯誤等情況。
當一個非同步函式正在等待Promise返回值的時候,當Promise方法報了錯誤的時候,它會丟擲異常,這個異常可以在catch方法裡面捕獲到。
在使用Async/Await時,我們通常使用try/catch語句進行異常捕獲。
try{
//do something
}
catch{
// deal err
}
我不是一個擁有強型別語音背景的人,所以額外的try/catch語句對我來說給我增加了額外的程式碼,這在我看來非常的冗餘不乾淨。我相信這可能是個人喜好的原因,但這是我對此的看法。
所以之前的程式碼看起來像這樣:
async function asyncTask(cb) {
try {
const user = await UserModel.findById(1);
if(!user) return cb('No user found');
} catch(e) {
return cb('Unexpected error occurred');
}
try {
const savedTask = await TaskModel({userId: user.id, name: 'Demo Task'});
} catch(e) {
return cb('Error occurred while saving task');
}
if(user.notificationsEnabled) {
try {
await NotificationService.sendNotification(user.id, 'Task Created');
} catch(e) {
return cb('Error while sending notification');
}
}
if(savedTask.assignedUser.id !== user.id) {
try {
await NotificationService.sendNotification(savedTask.assignedUser.id, 'Task was created for you');
} catch(e) {
return cb('Error while sending notification');
}
}
cb(null, savedTask);
}
一種不同的處理方式:
最近我一直在使用go-lang進行編碼,並且非常喜歡他們的解決方案,它的程式碼看起來像這樣:
data, err := db.Query("SELECT ...")
if err != nil { return err }
我認為它比使用try-catch語句塊更加簡潔,並且程式碼量更少,這使得它可讀和可維護更好。
但是使用Await的話,如果沒有為其提供try-catch處理異常的話,當程式發生錯誤的時候,它會默默的退出(你看到不丟擲的異常)。假如你沒有提供catch語句來捕捉錯誤的話,你將無法控制它。
當我和Tomer Barnea(我的好朋友)坐在一起並試圖找到一個更簡潔的解決方案時,我們得到了下一個使用方法:
請記住,Await在等待一個Promise返回值
有了這些知識,我們就可以製作一個小的通用函式來幫助我們捕捉這些錯誤。
// to.js
export default function to(promise) {
return promise.then(data => {
return [null, data];
})
.catch(err => [err]);
}
這個通用函式接收一個Promise,然後將處理成功的返回值以陣列的形式作為附加值返回,並且在catch方法中接收到捕捉到的第一個錯誤。
import to from './to.js';
async function asyncTask(cb) {
let err, user, savedTask;
[err, user] = await to(UserModel.findById(1));
if(!user) return cb('No user found');
[err, savedTask] = await to(TaskModel({userId: user.id, name: 'Demo Task'}));
if(err) return cb('Error occurred while saving task');
if(user.notificationsEnabled) {
const [err] = await to(NotificationService.sendNotification(user.id, 'Task Created'));
if(err) return cb('Error while sending notification');
}
cb(null, savedTask);
}
上面的例子只是一個使用該解決方案的簡單用例,你可以在io.js中新增攔截方法(類似除錯的斷點),該方法將接收原始錯誤物件,列印日誌或者進行其他任何你想要進行的操作,然後再返回操作後的物件。
我們為這個庫建立了一個簡單的NPM包,您可以使用以下方法進行安裝:
Github Repo
npm i await-to-js
這篇文章只是尋找Async/Await功能的一種不同方式,完全基於個人意見。 您可以使用Promise,僅使用try-catch和許多其他解決方案來實現類似的結果。 只要你喜歡並且它適用。