1. 程式人生 > >如何在Javascript中優雅的使用Async和Await進行錯誤的處理?

如何在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和許多其他解決方案來實現類似的結果。 只要你喜歡並且它適用。