從不用try-catch實現(xiàn)的async/await語法來說說錯誤處理
前不久看到 Dima Grossman 寫的 How to write async await without try-catch blocks in Javascript??吹綐祟}的時候,我感到非常好奇。我知道雖然在異步程序中可以不使用 try-catch 配合 async/await 來處理錯誤,但是處理方式并不能與 async/await 配合得很好,所以很想知道到底有什么辦法會比 try-catch 更好用。
Dima 去除 try-catch 的方法
當然套路依舊,Dima 講到了回調(diào)地獄,Promise 鏈并最終引出了 async/await。而在處理錯誤的時候,他并不喜歡 try-catch 的方式,所以寫了一個 to(promise) 來對 Promise 進行封裝,輔以解構(gòu)語法,實現(xiàn)了同步寫法但類似 Node 錯誤標準的代碼。摘抄代碼如下
- // to.js
- export default function to(promise) {
- return promise
- .then(data => {
- return [null, data];
- })
- .catch(err => [err]);
- }
應(yīng)用示例:
- 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 的辦法讓人產(chǎn)生的了熟悉的感覺,Node 的回調(diào)中不是經(jīng)常都這樣寫嗎?
- (err, data) => {
- if (err) {
- // deal with error
- } else {
- // deal with data
- }
- }
所以這個方法真的很有意思。不過回過頭來想一想,這段代碼中每當遇到錯誤,都是將錯誤消息通過 cb() 調(diào)用推出去,同時中斷后續(xù)過程。像這種中斷式的錯誤處理,其實正適合采用 try-catch。
使用 try-catch 改寫上面的代碼
要用 try-catch 改寫上面的代碼,首先要去掉 to() 封裝。這樣,一旦發(fā)生錯誤,需要使用 Promise.prototype.catch() 進行捕捉,或者使用 try-catch 對 await promise 語句進行捕捉。捕捉到的,當然是每個業(yè)務(wù)代碼里 reject 出來的 err。
然而注意,上面的代碼中并沒有直接使用 err,而是使用了自定義的錯誤消息。所以需要對 reject 出來的 err 進一步處理成指定的錯誤消息。當然這難不到誰,比如
- someAsync().catch(err => Project.reject("specified message"));
然后再最外層加上 try-catch 就好。所以改寫之后的代碼是:
- 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) {} 結(jié)構(gòu)。不習(xí)慣使用 try-catch 的程序員找找不到中斷點,但習(xí)慣了 try-catch 的程序員都知道,業(yè)務(wù)過程中一旦發(fā)生錯誤(異步代碼里指 reject),代碼就會跳到 catch 塊去處理 reject 出來的值。
但是,一般業(yè)務(wù)代碼 reject 出來的信息通常都是有用的。假如上面的每個業(yè)務(wù) 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) 實際上處理了兩個業(yè)務(wù):一是捕捉會引起中斷的 err ,并將其轉(zhuǎn)換為錯誤消息,二是通過 return 中斷業(yè)務(wù)過程。所以當 err 轉(zhuǎn)換為錯誤消息這一過程不再需要的時候,這種捕捉中斷再重新引起中斷的處理主顯得多余了。
繼續(xù)改進
用函數(shù)表達式改善 try-catch 邏輯
當然還有改進的空間,比如 try {} 塊中的代碼比較長,會造成閱讀不太方便,try-catch 的邏輯有被“切斷”的感覺。這種情況下可以使用函數(shù)表達式來改善
- 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);
- }
- }
如果對錯誤的處理代碼比較長,也可以寫成單獨的函數(shù)表達式。
如果過程中每一步的錯誤處理邏輯不同怎么辦
如果發(fā)生錯誤,不再轉(zhuǎn)換為錯誤消息,而是特定的錯誤處理邏輯,怎么辦?
思考一下,我們用字符串來表示錯誤消息,以后可以通過 console.log() 來處理處理。而邏輯,最適合的表示當然是函數(shù)表達式,最終可以通過調(diào)用來進行統(tǒng)一處理
- 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());
- }
- }
甚至還可以處理更復(fù)雜的情況
現(xiàn)在應(yīng)該都知道 .catch(err => Promise.reject(xx)),這里的 xx 就是 try-catch 的 catch 塊捕捉到的對象,所以如果不同的業(yè)務(wù) reject 出來不同的對象,比如有些是函數(shù)(表示錯誤處理邏輯),有些是字符串(表示錯誤消息),有些是數(shù)字(表示錯誤代碼)——其實只需要改 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
- }
- }
小結(jié)
我沒有批判 Dima 的錯誤處理方式,這個錯誤處理方式很好,很符合 Node 錯誤處理的風(fēng)格,也一定會受到很多人的喜愛。由于 Dima 的錯誤處理方式給帶靈感,同時也讓我再次審視了一直比較喜歡的 try-catch 方式。
用什么方式取決于適用場景、團隊約定和個人喜好等多種因素,在不同的情況下需要采用不同的處理方式,并不是說哪一種就一定好于另一種——合適的才是***的!