await-to-js,优雅地捕获 await 异常

1,576 阅读4分钟

我正在参与掘金会员专属活动-源码共读第一期,点击参与

前言

我们在使用 async...await 进行异步编程的时候,如果当前异步操作失败了,为了不影响后面代码的执行,就需要将 await 语句放在 try...catch 语句里面。

那么整个程序上,就可能会有很多个 try...catch语句 ,代码上不美观,可读性也很差。

所以 await-to-js 库就是为了解决这个问题,让代码能够优雅地捕获 await 异常。

GitHub 地址:github.com/scopsy/awai…

也可以用 github1s 访问,速度更快:github1s.com/scopsy/awai…

使用方法

在进行源码分析之前,我们需要熟悉它的用法。

首先来回顾一下,常规捕获 await 异常是怎么样的:

const UserModel = {
    findById: (userId) => {
        return new Promise((resolve, reject) => {
            if (userId) {
                const userObjet = {
                    id: userId,
                    notificationsEnabled: true,
                };

                resolve(userObjet);
            }

            reject("Data is missing");
        });
    },
};

async function asyncTask() {
    try {
        const user = await UserModel.findById(null);
    } catch (error) {
        console.log(error);  // Data is missing
    }
}
asyncTask();

UserModel.findById(null) 可以看作是我们平时在项目中调用的异步操作的 api 函数。一般情况下, await 语句后面跟的是 Promise 对象,并返回该对象的结果,如果要捕获 reject 返回的错误,就需要用到 try...catch 中的 catch 来接收这个错误,让后面的代码能够继续运行下去,否则的话,就会抛出异常,整个程序停止运行。

再来看看 await-to-js 是怎么捕获 await 异常的

首先下载 await-to-js 库到项目依赖中:

npm i await-to-js --save

引入并使用 await-to-js

import to from 'await-to-js';

const UserModel = {
    findById: (userId) => {
        return new Promise((resolve, reject) => {
            if (userId) {
                const userObjet = {
                    id: userId,
                    notificationsEnabled: true,
                };

                resolve(userObjet);
            }

            reject("Data is missing");
        });
    },
};
async function asyncTask() {
    const [err, data] = await to(UserModel.findById(null));
    console.log(err); // "Data is missing"
    console.log(data); // undefined
}
asyncTask();

将调用异步函数(UserModel.findById(null))后返回的结果当作 await-to-js(to) 函数的参数传入,然后用有两个元素的数组接收 to 函数返回的结果,第一个元素 err 是失败返回的结果,第二个元素是 data 成功后返回的结果。

可见 await-to-js 只用了一行代码就完成了 5 行代码的捕获异常。

再来看看,成功返回结果的情况:

async function asyncTask() {
    const [err, data] = await to(UserModel.findById(1));
    console.log(err); // undefined
    console.log(data); // {id: 1, notificationsEnabled: true}
}
asyncTask();

非常完美地获取到了 resolve 返回的结果。

源码分析

await-to-jsTypeScript 代码,代码量比较少,不到 15 行,即使没有使用过 TypeScript 的开发人员,也能看懂其中的奥妙。

export function to<T, U = Error> (
    promise: Promise<T>,
    errorExt?: object
): Promise<[U, undefined] | [null, T]> {
    return promise
        .then<[null, T]>((data: T) => [null, data])
        .catch<[U, undefined]>((err: U) => {
            if (errorExt) {
                const parsedError = Object.assign({}, err, errorExt);
                return [parsedError, undefined];
            }

            return [err, undefined];
        });
}

也可以在编译器的终端输入 npm run buildyarn build,将 TypeScript 代码编译成 JavaScript 代码:

function to(promise, errorExt) {
    return promise
        .then(function (data) {
            return [null, data];
        })
        .catch(function (err) {
            if (errorExt) {
                var parsedError = Object.assign({}, err, errorExt);
                return [parsedError, undefined];
            }
            return [err, undefined];
        });
}

源码的核心思想是:异步操作的成功结果通过 then 方法接收,失败结果通过 catch 方法接收,两个方法都用 return 语句将结果返回,这样的话,无论是 then 方法还是 catch 方法返回的新 Promise 对象的状态都是 fulfilled (成功的状态),因此能够被 await 正常接收。

对源码的疑问

关于 catch 方法里的 errorExt,不是特别明白,因为 err 可能是字符串,而 errorExt 是对象(TypeScript 源码中声明了 errorExt 是对象类型),那... Object.assign({}, err, errorExt) 语句是什么意思呢?字符串能够和对象进行合并吗?我们来试试看:

const err = 'failed';
const errorExt = {msg: 'error'}
Object.assign({}, err, errorExt)

得到的结果是:

{
    0: "f",
    1: "a",
    2: "i",
    3: "l",
    4: "e",
    5: "d",
    msg: "error"
}

使用 Object.assign 将字符串和对象进行合并时,会把字符串转换为——键为字符的索引,值为字符的对象。虽然结果上看字符串可以和对象合并,但是合并出来的结果...呃呃呃,还是看不懂。

个人觉得应该对 errerrorExt 进行数据类型判断,如果都是对象类型,那么才进行合并:

if (errorExt && typeof err === 'object') {
    var parsedError = Object.assign({}, err, errorExt);
    return [parsedError, undefined];
}

以上仅个人想法,欢迎到评论区留言讨论。

总结

await-to-js 主要运用了 thencatch 方法来接收结果,并通过 return 返回处理后的结果,这样新 Promise 对象的状态都是 fulfilled,结果被 await 正常接收,这就能够在不使用 try...catch 语句的情况下,捕获失败的情况。