从不用 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 更好用。javascript

Dima 去除 try-catch 的方法

固然套路依旧,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 改写上面的代码

要用 try-catch 改写上面的代码,首先要去掉 to() 封装。这样,一旦发生错误,须要使用 Promise.prototype.catch() 进行捕捉,或者使用 try-catch 对 await promise 语句进行捕捉。捕捉到的,固然是每一个业务代码里 reject 出来的 errasync

然而注意,上面的代码中并无直接使用 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-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());
    }
}

甚至还能够处理更复杂的状况

如今应该都知道 .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 方式。

用什么方式取决于适用场景、团队约定和我的喜爱等多种因素,在不一样的状况下须要采用不一样的处理方式,并非说哪种就必定好于另外一种——合适的才是最好的!

相关文章
相关标签/搜索