• 从不用 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 => Promise.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());
        }
    }

    甚至还可以处理更复杂的情况

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

    用什么方式取决于适用场景、团队约定和个人喜好等多种因素,在不同的情况下需要采用不同的处理方式,并不是说哪一种就一定好于另一种——合适的才是最好的!

    原博客地址:https://segmentfault.com/a/1190000011802045
  • 相关阅读:
    winform_webApiSelfHost
    手机访问网站自动跳转到手机版
    Linux常用命令
    css居中
    固定菜单
    BOM操作写法实例
    表格(合并单元格)
    倒计时
    分享到插件
    jquery获得下拉框的值
  • 原文地址:https://www.cnblogs.com/aidixie/p/14809296.html
Copyright © 2020-2023  润新知