【源码共读】第21期 | await-to-js 如何优雅的捕获 await 的错误

1,148 阅读4分钟

本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。

【若川视野 x 源码共读】第21期 | await-to-js 如何优雅的捕获 await 的错误 点击了解本期详情一起参与

await-to-js是什么

按照作者的说法,Promise解决了异步回调地狱的写法缺点,async await解决了Promise链式调用不直观的的缺点。而await-to-js解决了async await无法捕获异常的缺点。不用到处出现try catch

先看源码

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

export default to;

虽然代码不难,但是毕竟是TS,看起来不舒服,我们build一下。

image.png

build结束,出现以上文件,es5我知道是什么,但是umd我第一次见到,一般常见的是cjs esm。查了之后发现umd非同小可。 UMD(统一模块定义) :这种模块语法会自动监测开发人员使用的是 Common.js/AMD/import/export 种的哪种方式,然后再针对各自的语法进行导出,这种方式可以兼容所有其他的模块定义方法。我看下打包后的代码。

(function (global, factory) {
  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
  typeof define === 'function' && define.amd ? define(['exports'], factory) :
  (factory((global.awaitToJs = {})));
}(this, (function (exports) { 'use strict';

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

exports.to = to;
exports['default'] = to;

Object.defineProperty(exports, '__esModule', { value: true });

})));

这是一个自执行函数,通过exports module define exports来判断当前是 CommonJS 还是 AMD 生态系统,而且package.json中也默认设置umd导出。

image.png

不论是什么,构建完的核心代码我们是拿到了

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

源码依旧不难,to函数封装了一层promise,如果promisefulfilled时,返回的错误信息为null,并将结果放入第二项。如果promiserejected,返回的结果第二项为undefined,第一项为错误信息。如果传入自定义错误信息会和原生错误信息合并。引用此代码之后,可以依照以下样式写代码。


import to from './to.js';

async function asyncTask() {
    let err, user, savedTask;

    [err, user] = await to(UserModel.findById(1));
    if(!user) throw new CustomerError('No user found');

    [err, savedTask] = await to(TaskModel({userId: user.id, name: 'Demo Task'}));
    if(err) throw new CustomError('Error occurred while saving task');

   if(user.notificationsEnabled) {
      const [err] = await to(NotificationService.sendNotification(user.id, 'Task Created'));  
      if (err) console.error('Just log the error and continue flow');
   }
}

我唯一不理解的是作者的API设计,为什么一定要返回数组,而不是对象,如果返回对象那结构获取值的时候就不需要在意顺序了,唯一的缺点就是字段名称是定死的。我们可以修改下源码按照自己的思路调试调用。

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

async function foo() {
   const reject = Promise.reject();

   let { err, data } = await to(reject, { msg: '这个promise reject了' });
   console.log(err, data)
   const resolve = Promise.resolve(1);
   let { err: error, data: resolveData } = await to(resolve, { msg: '这个promise resolve了' });
   console.log(error, resolveData)
   const promise = Promise.resolve(2);
   let { err: e, data: resData } = await to(promise, { msg: '这个promise' });
   console.log(e, resData)
}

async function bar() {
   const reject = Promise.reject();
   let err, data
   ({ err, data } = await to(reject, { msg: '这个promise reject了' }));
   console.log(err, data)
   const resolve = Promise.resolve(3);
   ({ err, data } = await to(resolve, { msg: '这个promise resolve了' }));
   console.log(err, data)
   const promise = Promise.resolve(4);
   ({ err, data } = await to(promise, { msg: '这个promise' }));
   console.log(err, data)
}

async function baz() {
   const reject = Promise.reject();
   var { err, data } = await to(reject, { msg: '这个promise reject了' });
   console.log(err, data)
   const resolve = Promise.resolve(5);
   var { err, data } = await to(resolve, { msg: '这个promise resolve了' });
   console.log(err, data)
   const promise = Promise.resolve(6);
   var { err, data } = await to(promise, { msg: '这个promise' });
   console.log(err, data)
}

修改过源码调试发现只有以上三种方式解构获取,各有缺点。

  • foo方法几乎需要给每一个结构获取的值设置别名,出力不讨好。
  • bar方法需要前后用括号括起来,而不像[ ]结构赋值可以直接使用,太繁琐。
  • baz要用var来定义变量,这明显是开历史倒车,坚决反对。

综上所述原作者设计返回数组,可以随意设置data别名,而且await-to-js本来目的就是处理错误信息,所以把错误信息err放在首位也无可厚非。

总结

  • await-to-js可以优雅处理async await异常,其实可以二次封装,用统一的方法处理异常
  • 在设计API的时候,如果返回值数量固定,有时候返回数组比返回对象更合适。
  • 代码中出现大量繁琐重复的不优雅的代码,就可以考虑用各种方法封装,方便维护。