本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
【若川视野 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一下。
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导出。
不论是什么,构建完的核心代码我们是拿到了
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,如果promise为fulfilled时,返回的错误信息为null,并将结果放入第二项。如果promise为rejected,返回的结果第二项为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的时候,如果返回值数量固定,有时候返回数组比返回对象更合适。
- 代码中出现大量繁琐重复的不优雅的代码,就可以考虑用各种方法封装,方便维护。