JS await 异常捕获类
JavaScript 异步异常处理 三 种姿势,try/catch 嵌套地狱 最常见也最糟糕。
- try/catch 包 await:层层嵌套,业务和错误处理混成一坨
- .catch() 链式:返回 undefined 难以判断错误来源
- 元组解构
[err, data]:Go 风格,推荐
封装一个 to(promise) 工具函数,10 行代码消灭所有 try/catch,错误处理变成一行 if (err) return。
关键:业务错误用元组、致命错误仍要抛,别用工具类把所有异常都吞掉。
问题:try/catch 把代码切成稀碎
写 async/await 的人都遇过这种代码:
async function getUserOrders(uid) {
let user;
try {
user = await fetchUser(uid);
} catch (e) {
console.error('fetch user failed', e);
return null;
}
let orders;
try {
orders = await fetchOrders(user.id);
} catch (e) {
console.error('fetch orders failed', e);
return null;
}
return { user, orders };
}
20 行代码,业务逻辑只占 3 行,剩下全是异常处理样板。函数一多,整个项目就被 try/catch 切成稀碎。
用 .catch() 也不行
有人会说,那就别 try/catch,直接 .catch():
const user = await fetchUser(uid).catch(() => null);
if (!user) return null;
看着干净了,但有两个坑:
.catch(() => null)把错误吞了,线上排查问题时一脸懵- 返回
null时,到底是"用户不存在"还是"请求失败"分不清
解法:Go 风格的元组返回
借鉴 Go 的 value, err := fn() 模式,封装一个工具函数 to:
export function to<T, E = Error>(
promise: Promise<T>,
): Promise<[E, undefined] | [null, T]> {
return promise
.then<[null, T]>((data) => [null, data])
.catch<[E, undefined]>((err) => [err, undefined]);
}
核心思路是把 Promise 的两种结果都转成 [err, data] 元组,永远 resolve,永远不 reject。
业务代码立刻变成这样:
async function getUserOrders(uid) {
const [userErr, user] = await to(fetchUser(uid));
if (userErr) return null;
const [ordersErr, orders] = await to(fetchOrders(user.id));
if (ordersErr) return null;
return { user, orders };
}
没有 try/catch,没有嵌套,错误判断一行搞定。
进阶:附加自定义错误信息
实际项目里,错误信息往往需要扩展,比如带上请求参数、调用栈位置。给 to 加第二个参数:
export function to<T, E = Error>(
promise: Promise<T>,
errorExt?: Record<string, any>,
): Promise<[E, undefined] | [null, T]> {
return promise
.then<[null, T]>((data) => [null, data])
.catch<[E, undefined]>((err: E) => {
if (errorExt) {
const parsedError = Object.assign({}, err, errorExt);
return [parsedError, undefined];
}
return [err, undefined];
});
}
调用:
const [err, data] = await to(fetchUser(uid), { uid, scene: 'profile-page' });
if (err) {
logger.error('fetch user failed', err); // err 上挂着 uid 和 scene
return null;
}
排查问题时,日志里直接有上下文,不用再去翻调用链。
什么时候不该用
to 不是银弹。以下场景别用:
- 致命错误:数据库连接断了、配置文件缺失,这种应该让进程崩掉,不该吞
- 业务断言:参数校验失败,直接
throw new ValidationError,让上层统一处理 - Promise.all 场景:
Promise.all一个 reject 全部失败,套to反而绕弯子,用Promise.allSettled更直接
to 适合的是"我知道这里可能失败,失败了我有备选方案"的场景,比如调用第三方 API、读取缓存、查询非关键数据。
一个常见误区
// ❌ 别这么写
const [err, data] = await to(somePromise);
if (err) {
throw err; // 转一圈又抛出去了,那要 to 干嘛?
}
如果错误最终还是要往上抛,就别用 to,直接 await 让它自然冒泡,外层 try/catch 或者全局 errorHandler 兜底。to 的价值是"本地消化错误",一旦你还想抛,工具就失去意义了。
社区现成方案
npm 上有 await-to-js,1KB 不到,源码和上面几乎一样。直接 pnpm add await-to-js 即可,不用自己维护。
但我更推荐复制粘贴进项目里 —— 一个 10 行的函数,不值得多一个依赖。