Skip to main content

JS await 异常捕获类

· 4 min read

JavaScript 异步异常处理 种姿势,try/catch 嵌套地狱 最常见也最糟糕。

  1. try/catch 包 await:层层嵌套,业务和错误处理混成一坨
  2. .catch() 链式:返回 undefined 难以判断错误来源
  3. 元组解构 [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;

看着干净了,但有两个坑:

  1. .catch(() => null) 把错误吞了,线上排查问题时一脸懵
  2. 返回 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 不是银弹。以下场景别用:

  1. 致命错误:数据库连接断了、配置文件缺失,这种应该让进程崩掉,不该吞
  2. 业务断言:参数校验失败,直接 throw new ValidationError,让上层统一处理
  3. 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 行的函数,不值得多一个依赖。