天天看點

從不用 try-catch 實作的 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 講到了回調地獄,Promise 鍊并最終引出了 async/await。而在處理錯誤的時候,他并不喜歡 try-catch 的方式,是以寫了一個

to(promise)

來對 Promise 進行封裝,輔以解構文法,實作了同步寫法但類似 Node 錯誤标準的代碼。摘抄代碼如下

// 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 的回調中不是經常都這樣寫嗎?

(err, data) => {
    if (err) {
        // deal with error
    } else {
        // deal with data
    }
}           

是以這個方法真的很有意思。不過回過頭來想一想,這段代碼中每當遇到錯誤,都是将錯誤消息通過

cb()

調用推出去,同時中斷後續過程。像這種中斷式的錯誤處理,其實正适合采用 try-catch。

使用 try-catch 改寫上面的代碼

要用 try-catch 改寫上面的代碼,首先要去掉

to()

封裝。這樣,一旦發生錯誤,需要使用

Promise.prototype.catch()

進行捕捉,或者使用 try-catch 對

await promise

語句進行捕捉。捕捉到的,當然是每個業務代碼裡

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) {}

結構。不習慣使用 try-catch 的程式員找找不到中斷點,但習慣了 try-catch 的程式員都知道,業務過程中一旦發生錯誤(異步代碼裡指 reject),代碼就會跳到

catch

塊去處理 reject 出來的值。

但是,一般業務代碼 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-catch 邏輯

當然還有改進的空間,比如

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());
    }
}           

甚至還可以處理更複雜的情況

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
        }
    }           

小結

繼續閱讀