32.promise-梳理 Promise 错误处理
# 0.前言
本节博客代码仓库 (opens new window)
本篇博客用于梳理异步任务拦截错误操作,此前在网络上搜索出来的文章对异步的手法都是 try...catch
一把梭,甚至还有一篇文章《如何给所有的 async 函数添加 try/catch?》 (opens new window) 教如何使用babel
插件 + AST
手法去自动添加的操作(大佬就是 6)。
此时就产生一个疑问了,是不是所有的 async
都需要添加 try...catch
这一种写法呢?有没有其他写法呢?异步任务的错误拦截有没有更优雅的方案呢?
因此本篇博客就对这一块的知识点进行一个简单的梳理。
# 1. 场景
对于异步任务的处理,首先要区分是串行的还是并行的。
并行场景:
async function main() {
const [resA, resB, resC] = await Promise.all([
asyncErrA(),
asyncErrB(),
asyncErrC(),
]);
}
2
3
4
5
6
7
串行场景:
注:这里统一采用
async-await
语法来维护同步写法。
async function asyncErrA() {
throw new Error("ErrorA 出错了");
}
async function asyncErrB() {
throw new Error("ErrorB 出错了");
}
async function asyncErrC() {
throw new Error("ErrorC 出错了");
}
async function main() {
const resA = await asyncErrA();
const resB = await asyncErrB();
const resC = await asyncErrC();
}
main();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 2. 并行解决方案
对于并行场景,直接将 Promise.all
替换为 Promise.allSettled
即可。
# 3.串行解决方案
# 3.1 添加统一的错误拦截
将代码片段通过 try...catch
进行包裹
async function main() {
......
try {
const asyncFuncARes = await asyncErrA();
const asyncFuncBRes = await asyncErrB();
const asyncFuncCRes = await asyncErrC();
} catch (error) {}
......
}
main();
2
3
4
5
6
7
8
9
10
【推荐】如果是一整块代码片段的话,可直接使用 .then
处理
async function main() {
const resA = await asyncErrA();
const resB = await asyncErrB();
const resC = await asyncErrC();
}
main()
.then((v) => console.log(v))
.catch((e) => {});
2
3
4
5
6
7
8
9
如果报错非常规整的话,可以使用 switch...case
进行处理
async function main() {
const asyncFuncARes = await asyncErrA();
const asyncFuncBRes = await asyncErrB();
const asyncFuncCRes = await asyncErrC();
}
try {
main();
} catch (error) {
switch (error) {
case "[Error: ErrorA 出错了]":
/* todo: 处理 A 出错逻辑 */
break;
case "[Error: ErrorB 出错了]":
/* todo: 处理 B 出错逻辑 */
break;
case "[Error: ErrorC 出错了]":
/* todo: 处理 C 出错逻辑 */
break;
default:
break;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 3.2 为所有异步任务添加错误拦截
统一拦截的方案存在的问题是,当 asyncA => asyncB => asyncC
执行时,假设 asyncA
存在报错,asyncB
和 async
就不再继续执行了,如果希望做到更加细粒度的控制可以为每个异步任务进行错误拦截。
目前有两种方案可以添加拦截:
- 直接使用
catch
async function main() {
const asyncFuncARes = await asyncErrA().catch((err) => {});
const asyncFuncBRes = await asyncErrB().catch((err) => {});
/* 如果 asyncErrC 成功返回值为 "XXX", 那 asyncFuncCRes 既有可能是 "XXX" 也有可能是 "YYY" */
const asyncFuncCRes = await asyncErrC().catch((err) => return "YYY");
}
main();
2
3
4
5
6
7
但此写法会产生一个心智负担,就是返回值 asyncFunXRes
既有可能是成功的结果,也有可能是 catch
中返回的结果。开发者在对后续结果进行处理的有可能会产生歧义。
- 为所有函数添加
try...catch
async function main() {
try {
const asyncFuncARes = await asyncErrA();
} catch (error) {}
try {
const asyncFuncBRes = await asyncErrB();
} catch (error) {}
try {
const asyncFuncCRes = await asyncErrC();
} catch (error) {}
}
main();
2
3
4
5
6
7
8
9
10
11
12
使用 try...catch
将成功或者失败就分的很清楚,asyncFuncXRes
一定是成功的返回值。
但是此类写法的缺点就是代码结构太乱非常影响整体逻辑的阅读,下一节将使用一个第三方库来解决这个问题。
# 4. await-to-j(t)s
在 Node
的开发中的 API
设计中所有 api
的 callback
回调的第一个参数必然是 err
,看一个例子:
import { access, constants } from "node:fs";
const file = "package.json";
// 检查 file 是否存在在当前的工作区中?
access(file, constants.R_OK, (err) => {
console.log(`${file} ${err ? "is not readable" : "is readable"}`);
});
2
3
4
5
6
7
8
那可不可以也设计一个函数将异步 Promise
也改造成此类写法,其实社区中已存在解决方案,就是 await-to-js/ts
方案,具体的做法就是使用 to
函数将原有包裹一层:
import { to } from "await-to-ts";
async function main() {
const [errA, asyncFuncARes] = await to(asyncErrA());
if (errA) {
/* 对错误进行处理*/
}
const [errB, asyncFuncBRes] = await to(asyncErrB());
const [errC, asyncFuncCRes] = await to(asyncErrC());
}
main();
2
3
4
5
6
7
8
9
10
该库的思想非常简单,我们都知道 Promise
类在pending
状态是永远无法工作的,那该函数的目的就保证 promise
一定是存在状态不就好了。
如果自己封装的可以这么做:
function wrapAsyncFunc(asyncFunc) {
return new Promise((resolve, reject) => {
asyncFunc()
.then(resolve)
.catch(reject);
});
}
async function main() {
const p1 = wrapAsyncFunc(asyncErrA);
const p2 = wrapAsyncFunc(asyncErrB);
const p3 = wrapAsyncFunc(asyncErrC);
const [resA, resB, resC] = await Promise.allSettled([p1, p2, p3]);
console.log(resA.value, resB.value, resC.value);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
如果更进一步的话:
- 如果
fillfilled
的话,返回[null, res]
- 如果
rejected
的话,返回[err, undefined]
export function to<T>(promise: Promise<T>) {
return promise
.then<[null, T]>((data: T) => [null, data])
.catch<[U, undefined]>((err: U) => {
return [err, undefined];
});
}
2
3
4
5
6
7
由于该库源码核心过少,在真实项目中完全可以手动实现作为工具库。
# 5. 总结
本篇主要对异步的串行和并行的拦截方案进行了一个梳理工作:
对于并行写法,直接使用
Promsie.allSettled
就够用了。对于串行写法,其实也大可不必所有都添加
try...catch
。如果是整体代码包裹的话,直接
main.then().catch()
就挺简洁。如果是为每一段异步都添加错误拦截的话,到是可以偏具
Node
回调风格的await-to-ts
库,并对该库也进行了简单实践。