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

856 阅读4分钟

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

1. 学习任务和目标

2. 学习过程

3.1 初识 await-to-js 这个 npm

Async await wrapper for easy error handling without try-catch。

翻译过来就是:无需 try-catch 即可轻松处理错误的异步等待包装器。

简单地回顾一下 JavaScript 异步编程的进化之路。

  • 第1阶段:早期的异步编程代码
    在回调中嵌套着回调,形成了回调地狱,这使得维护代码和管理控制流非常困难:
function AsyncTask() {
   asyncFuncA(function(err, resultA){
      if(err) return cb(err);

      asyncFuncB(function(err, resultB){
         if(err) return cb(err);

          asyncFuncC(function(err, resultC){
               if(err) return cb(err);

               // And so it goes....
          });
      });
   });
}
  • 第2阶段:Promise 阶段

ES6 的 Promise 是一种优雅的异步编程解决方案,允许我们链式调用,解决了传统的回调地狱的问题。 异步编程可以写成如下格式:

function asyncTask(cb) {
   asyncFuncA.then(AsyncFuncB)
      .then(AsyncFuncC)
      .then(AsyncFuncD)
      .then(data => cb(null, data)
      .catch(err => cb(err));
}

相比较于上面的回调地狱,使用 Promise 优雅了很多,可还是存在两个不足的地方

  1. 不够同步(代码依然会纵向延伸)
  2. 不能给每一次异步操作都进行错误处理  (这也就是为什么ES7中会出现 async/await,号称异步编程的最后解决方案的原因了)
  • 第3阶段: ES7 Async/await

Note: You will need to use a transpiler in order to enjoy async/await, you can use either babel or typescript to the polyfills required.

async函数是Generator函数的语法糖。使用 关键字async来表示,在函数内部使用await来表示异步。相较于Generatorasync函数的改进在于下面四点:

  • 内置执行器Generator函数的执行必须依靠执行器,而async函数自带执行器,调用方式跟普通函数的调用一样
  • 更好的语义asyncawait相较于*yield更加语义化
  • 更广的适用性co模块约定,yield命令后面只能是 Thunk 函数或 Promise对象。而async函数的await命令后面则可以是 Promise 或者 原始类型的值(Number,string,boolean,但这时等同于同步操作)
  • 返回值是 Promiseasync函数返回值是 Promise 对象,比 Generator 函数返回的 Iterator 对象方便,可以直接使用then()方法进行调用

因此,代码可以被改写成这样:

async function asyncTask(cb) {
  const asyncFuncARes = await asyncFuncA();
  const asyncFuncBRes = await asyncFuncB(asyncFuncARes);
  const asyncFuncCRes = await asyncFuncC(asyncFuncBRes);
}

可以对每一次异步操作进行错误处理

async function asyncTask(cb) {
    try {
        const asyncFuncARes = await asyncFuncA();
    } catch (error) {
        return new Error(error);
    }

    try {
        const asyncFuncBRes = await asyncFuncB(asyncFuncARes);
    } catch (error) {
        return new Error(error);
    }

    try {
        const asyncFuncCRes = await asyncFuncC(asyncFuncBRes);
    } catch (error) {
        return new Error(error);
    }
}

虽然优化了 promise 的不足之处,但是每一次的异步操作都要用 try/catch 进行错误处理还是不够方便的。

  • 第4阶段:使用 await-to-js 库,代码改写成:
import to from './to.js';

async function asyncTask() {
  const [err1, asyncFuncARes] = await to(asyncFuncA());
  if (err1) throw new Error(err1);

  const [err2, asyncFuncBRes] = await to(asyncFuncB(asyncFuncARes));
  if (err2) throw new Error(err2);

  const [err3, asyncFuncCRes] = await to(asyncFuncC(asyncFuncBRes));
  if (err3) throw new Error(err3);
}

3.2 await-to-js 如何使用和源码分析

安装依赖:npm i await-to-js --save

使用:

import to from 'await-to-js';
// If you use CommonJS (i.e NodeJS environment), it should be:
// const to = require('await-to-js').default;

async function asyncTaskWithCb(cb) {
     let err, user, savedTask, notification;

     [ 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) {
       [ err ] = await to(NotificationService.sendNotification(user.id, 'Task Created'));
       if(err) return cb('Error while sending notification');
    }

    if(savedTask.assignedUser.id !== user.id) {
       [ err, notification ] = await to(NotificationService.sendNotification(savedTask.assignedUser.id, 'Task was created for you'));
       if(err) return cb('Error while sending notification');
    }

    cb(null, savedTask);
}

async function asyncFunctionWithThrow() {
  const [err, user] = await to(UserModel.findById(1));
  if (!user) throw new Error('User not found');
}

TS版本的源码:

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;

JS版本的源码:

export function to( promise, errorExt) {
  return promise
    .then((data) => [null, data]) // 成功,返回[null, 响应结果]
    .catch((err) => {
      if (errorExt) {
        const parsedError = Object.assign({}, err, errorExt);
        return [parsedError, undefined];  // 失败,返回[错误信息, undefined]
      }

      return [err, undefined];
    });
}
export default to;

从代码中可以看到

  • 无论成功还是失败,都返回一个数组;
  • 成功的话,返回 [null, 响应结果]
  • 失败的话,返回 [错误信息, undefined]
  • errorExt 是用户自定义错误信息,通过Object.assign将正常返回的 error 和用户自定义的合并到一个对象里面供用户自己选择。

3. 简单总结

确实是一个简短而精美的库,可以在项目中尝试使用。 我个人习惯了promise 链式调用写法,也觉得蛮方便的。 是否有必要引入使用,就看你的需求场景吧。