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

1,166 阅读3分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

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

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

本文涉及

源码地址:github.com/scopsy/awai…

官方博客:blog.grossman.io/how-to-writ…

分析库

先看ReadMe.md

image-20220916100410179

这个库是用来优雅的处理try...catch错误处理

在官方博客中提到回调地狱,使用了Promise来解决,并配合ES6async,await的写法,增加了代码的可读性。如果我们需要捕获Promise异常时,需要

async function asyncTask(cb) {
  try {
    const user = await UserModel.findById(1)
    if (!user) return cb('No user found')
  } catch (e) {
    return cb('Unexpected error occurred')
  }

  try {
    const savedTask = await TaskModel({ userId: user.id, name: 'Demo Task' })
  } catch (e) {
    return cb('Error occurred while saving task')
  }

  //...省略

  cb(null, savedTask)
}

存在的问题:

  • 多个try...catch...重复,可读性较差

  • 如果不定义catch函数,错误会抛出并且自动退出函数

作者的解决方案是借鉴了go的写法:

data, err := db.Query("SELECT ...")
if err != nil { return err }
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');
    }
}

代码可读性更强,我们看看源码是怎么实现的

源码分析

/**
 * @param { Promise } promise
 * @param { Object= } errorExt - Additional Information you can pass to the err object
 * @return { Promise }
 */
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];
    });
}

源码非常简短,就是将Promise封装了一层

成功 return [null,data],失败return [err,undefined]

用法

interface ServerResponse {
  test: number;
}

const p = Promise.resolve({test: 123});

const [err, data] = await to<ServerResponse>(p);
console.log(data.test);

封装之后跟hooks的用法相似,代码更加简洁,可读性增加

测试用例

import { to } from "../src/await-to-js";

describe("Await to test", async () => {
  // 成功返回结果  return <null,number>
  it("should return a value when resolved", async () => {
    const testInput = 41;
    const promise = Promise.resolve(testInput);

    const [err, data] = await to<number>(promise);

    expect(err).toBeNull();
    expect(data).toEqual(testInput);
  });

  it("should return an error when promise is rejected", async () => {
    // 失败处理结果 return <Error,undefined>
    const testInput = 41;
    const promise = Promise.reject("Error");

    const [err, data] = await to<number>(promise);

    expect(err).toEqual("Error");
    expect(data).toBeUndefined();
  });

  it("should add external properties to the error object", async () => {
    // 失败处理结果  return 自定义类型的error
    const promise = Promise.reject({ error: "Error message" });

    const [err] = await to<string, { error: string; extraKey: number }>(promise, {
      extraKey: 1,
    });

    expect(err).toBeTruthy();
    expect((err as any).extraKey).toEqual(1);
    expect((err as any).error).toEqual("Error message");
  });

  it("should receive the type of the parent if no type was passed", async () => {
    // 没有设置类型声明时,自动推导
    let user: { name: string };
    let err: Error;

    [err, user] = await to(Promise.resolve({ name: "123" }));

    expect(user.name).toEqual("123");
  });
});

总结

这个库给处理Promise异常提供了一种简洁优雅的用法,通过封装一个hooks来增加代码的可读性,作者从go的使用借鉴到js上,这种知识的迁移能力和意识也是值得我们学习的。

最后,非常感谢若川大佬的源码共读活动,拓宽了个人的视野和知识深度,这种能力也潜移默化的影响了个人的技术能力。