前不久看到 Dima Grossman 寫的 How to write async await without try-catch blocks in Javascript。看到標題的時候,我感到很是好奇。我知道雖然在異步程序中能夠不使用 try-catch 配合 async/await 來處理錯誤,可是處理方式並不能與 async/await 配合得很好,因此很想知道到底有什麼辦法會比 try-catch 更好用。javascript
固然套路依舊,Dima 講到了回調地獄,Promise 鏈並最終引出了 async/await。而在處理錯誤的時候,他並不喜歡 try-catch 的方式,因此寫了一個 to(promise)
來對 Promise 進行封裝,輔以解構語法,實現了同步寫法但相似 Node 錯誤標準的代碼。摘抄代碼以下java
// to.js export default function to(promise) { return promise .then(data => { return [null, data]; }) .catch(err => [err]); }
應用示例:程序員
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); }
Dima 的辦法讓人產生的了熟悉的感受,Node 的回調中不是常常都這樣寫嗎?promise
(err, data) => { if (err) { // deal with error } else { // deal with data } }
因此這個方法真的頗有意思。不過回過頭來想想,這段代碼中每當遇到錯誤,都是將錯誤消息經過 cb()
調用推出去,同時中斷後續過程。像這種中斷式的錯誤處理,其實正適合採用 try-catch。異步
要用 try-catch 改寫上面的代碼,首先要去掉 to()
封裝。這樣,一旦發生錯誤,須要使用 Promise.prototype.catch()
進行捕捉,或者使用 try-catch 對 await promise
語句進行捕捉。捕捉到的,固然是每一個業務代碼裏 reject
出來的 err
。async
然而注意,上面的代碼中並無直接使用 err
,而是使用了自定義的錯誤消息。因此須要對 reject 出來的 err
進一步處理成指定的錯誤消息。固然這難不到誰,好比函數
someAsync().catch(err => Promise.reject("specified message"));
而後再最外層加上 try-catch 就好。因此改寫以後的代碼是:prototype
async function asyncTask(cb) { try { const user = await UserModel.findById(1) .catch(err => Promise.reject("No user found")); const savedTask = await TaskModel({ userId: user.id, name: "Demo Task" }) .catch(err => Promise.reject("Error occurred while saving task")); if (user.notificationsEnabled) { await NotificationService.sendNotification(user.id, "Task Created") .catch(err => Promise.reject("Error while sending notification")); } cb(null, savedTask); } catch (err) { cb(err); } }
上面這段代碼,從代碼量上來講,並無比 Dima 的代碼減小了多少工做量,只是去掉了大量 if (err) {}
結構。不習慣使用 try-catch 的程序員找找不到中斷點,但習慣了 try-catch 的程序員都知道,業務過程當中一旦發生錯誤(異步代碼裏指 reject),代碼就會跳到 catch
塊去處理 reject 出來的值。code
可是,通常業務代碼 reject 出來的信息一般都是有用的。假如上面的每一個業務 reject 出來的 err 自己就是錯誤消息,那麼,用 Dima 的模式,仍然須要寫對象
if (err) return cb(err);
而用 try-catch 的模式,就簡單多了
async function asyncTask(cb) { try { const user = await UserModel.findById(1); const savedTask = await TaskModel({ userId: user.id, name: "Demo Task" }); if (user.notificationsEnabled) { await NotificationService.sendNotification(user.id, "Task Created"); } cb(null, savedTask); } catch (err) { cb(err); } }
爲何?由於在 Dima 的模式中,if (err)
實際上處理了兩個業務:一是捕捉會引發中斷的 err
,並將其轉換爲錯誤消息,二是經過 return
中斷業務過程。因此當 err
轉換爲錯誤消息這一過程再也不須要的時候,這種捕捉中斷再從新引發中斷的處理就顯得多餘了。
固然還有改進的空間,好比 try {}
塊中的代碼比較長,會形成閱讀不太方便,try-catch 的邏輯有被「切斷」的感受。這種狀況下可使用函數表達式來改善
async function asyncTask(cb) { async function process() { const user = await UserModel.findById(1); const savedTask = await TaskModel({ userId: user.id, name: "Demo Task" }); if (user.notificationsEnabled) { await NotificationService.sendNotification(user.id, "Task Created"); } return savedTask; } try { cb(null, await process()); } catch (err) { cb(err); } }
若是對錯誤的處理代碼比較長,也能夠寫成單獨的函數表達式。
若是發生錯誤,再也不轉換爲錯誤消息,而是特定的錯誤處理邏輯,怎麼辦?
思考一下,咱們用字符串來表示錯誤消息,之後能夠經過 console.log()
來處理處理。而邏輯,最適合的表示固然是函數表達式,最終能夠經過調用來進行統一處理
async function asyncTask(cb) { async function process() { const user = await UserModel.findById(1) .catch(err => Promise.reject(() => { // deal with error on looking for the user return "No user found"; })); const savedTask = await TaskModel({ userId: user.id, name: "Demo Task" }) .catch(err => Promise.reject(() => { // making model error // deal with it return err === 1 ? "Error occurred while saving task" : "Error occurred while making model"; })); if (user.notificationsEnabled) { await NotificationService.sendNotification(user.id, "Task Created") .catch(err => Promise.reject(() => { // just print a message logger.log(err); return "Error while sending notification"; })); } return savedTask; } try { cb(null, await process()); } catch (func) { cb(func()); } }
如今應該都知道 .catch(err => Promise.reject(xx))
,這裏的 xx
就是 try-catch 的 catch 塊捕捉到的對象,因此若是不一樣的業務 reject 出來不一樣的對象,好比有些是函數(表示錯誤處理邏輯),有些是字符串(表示錯誤消息),有些是數字(表示錯誤代碼)——其實只須要改 catch 塊就行
try { // ... } catch(something) { switch (typeof something) { case "string": // show message something break; case "function": something(); break; case "number": // look up something as code // and show correlative message break; default: // deal with unknown error } }
我沒有批判 Dima 的錯誤處理方式,這個錯誤處理方式很好,很符合 Node 錯誤處理的風格,也必定會受到不少人的喜好。因爲 Dima 的錯誤處理方式給帶靈感,同時也讓我再次審視了一直比較喜歡的 try-catch 方式。
用什麼方式取決於適用場景、團隊約定和我的喜愛等多種因素,在不一樣的狀況下須要採用不一樣的處理方式,並非說哪種就必定好於另外一種——合適的纔是最好的!