告别 try/catch 地狱:用三元组重新定义 JavaScript 错误处理
写了五年前端,我终于受够了 try/catch。
一个真实的场景
上周重构一个老项目,打开一个文件,200 行代码里有 47 行是 try/catch。
javascript
async function loadDashboard(userId) {
let user;
try {
user = await fetchUser(userId);
} catch (e) {
console.error('获取用户失败', e);
return;
}
let posts;
try {
posts = await fetchPosts(user.id);
} catch (e) {
console.error('获取文章失败', e);
return;
}
let comments;
try {
comments = await fetchComments(posts[0].id);
} catch (e) {
console.error('获取评论失败', e);
return;
}
render(user, posts, comments);
}
看着这坨代码,我问自己:这真的是 2024 年该有的写法吗?
javascript
// 😵 典型的 try/catch 地狱
async function processOrder(orderId) {
let order;
try {
order = await fetchOrder(orderId);
} catch (e) {
console.error('获取订单失败', e);
return;
}
let user;
try {
user = await fetchUser(order.userId);
} catch (e) {
console.error('获取用户失败', e);
return;
}
let payment;
try {
payment = await processPayment(order, user);
} catch (e) {
console.error('支付处理失败', e);
return;
}
let shipping;
try {
shipping = await createShipping(order, user);
} catch (e) {
console.error('创建物流失败', e);
return;
}
let notification;
try {
notification = await sendNotification(user, order);
} catch (e) {
console.error('发送通知失败', e);
return;
}
return { order, payment, shipping, notification };
}
// 50+ 行代码,真正的业务逻辑只有 5 行
问题出在哪?
try/catch 本身没错,但它有几个让人难受的地方:
1. 打断思维流
正常逻辑和错误处理交织在一起,读代码时要不断切换上下文。
2. 变量作用域问题
javascript
let data;
try {
data = await fetch();
} catch (e) {
// ...
}
// data 可能是 undefined,TypeScript 也帮不了你
3. 无法优雅组合
想把多个异步操作串起来?要么嵌套,要么一堆临时变量。
4. 错误类型丢失
catch 里的 e 是 unknown,每次都要手动判断类型。
Go 语言给的启发
Go 程序员处理错误的方式很不一样:
go
user, err := fetchUser(id)
if err != nil {
return err
}
错误不是被"抛出"的,而是作为返回值的一部分。这种方式有个好处:错误处理变成了数据流的一部分,而不是控制流的中断。
go
// ✅ Go 风格:错误是值,不是异常
func processOrder(orderId string) (*Result, error) {
order, err := fetchOrder(orderId)
if err != nil {
return nil, fmt.Errorf("获取订单失败: %w", err)
}
user, err := fetchUser(order.UserId)
if err != nil {
return nil, fmt.Errorf("获取用户失败: %w", err)
}
payment, err := processPayment(order, user)
if err != nil {
return nil, fmt.Errorf("支付处理失败: %w", err)
}
return &Result{Order: order, User: user, Payment: payment}, nil
}
// 逻辑清晰,错误处理紧跟调用,一目了然
把这个思路搬到 JavaScript
我写了一个小工具库 await-to-tuple,核心思想很简单:
把 Promise 的结果包装成 [ok, error, data] 三元组。
typescript
import { to } from 'await-to-tuple';
const [ok, err, user] = await to(fetchUser(id));
if (!ok) {
console.error('获取用户失败:', err.message);
return;
}
console.log(user.name); // TypeScript 知道这里 user 一定有值
为什么是三元组?
你可能见过 await-to-js 这个库,它返回 [err, data]。但这有个问题:
javascript
const [err, data] = await to(fetchUser());
// 如果 data 本身就是 undefined 或 null 呢?
// 你分不清是"成功但值为空"还是"失败了"
三元组通过第一个布尔值 ok 明确区分成功和失败:
| ok | err | data | 含义 |
|---|---|---|---|
| true | null | 值 | 成功 |
| false | Error | null | 失败 |
typescript
// ❌ 二元组的问题:await-to-js 风格
const [err, data] = await to(fetchUser());
if (err) { /* 处理错误 */ }
// 问题:如果 API 返回 null 或 undefined 怎么办?
// data 为空到底是"成功但值为空"还是"失败了"?
// ✅ 三元组的优势:await-to-tuple 风格
const [ok, err, data] = await to(fetchUser());
if (!ok) { /* 一定是失败 */ }
// ok 明确告诉你成功还是失败,不存在歧义
// 对比表:
// | ok | err | data | 含义 |
// |-------|-------|------|----------------|
// | true | null | 值 | 成功,有数据 |
// | true | null | null | 成功,值为空 |
// | false | Error | null | 失败 |
实际效果
还记得开头那 47 行代码吗?用 await-to-tuple 重写:
typescript
import { to } from 'await-to-tuple';
async function loadDashboard(userId: string) {
const [ok1, err1, user] = await to(fetchUser(userId));
if (!ok1) return console.error('获取用户失败:', err1.message);
const [ok2, err2, posts] = await to(fetchPosts(user.id));
if (!ok2) return console.error('获取文章失败:', err2.message);
const [ok3, err3, comments] = await to(fetchComments(posts[0].id));
if (!ok3) return console.error('获取评论失败:', err3.message);
render(user, posts, comments);
}
从 47 行变成 12 行。 逻辑清晰,错误处理紧跟在调用后面,一目了然。
类型安全是重点
await-to-tuple 完全用 TypeScript 写的,类型推导非常准确:
typescript
const [ok, err, user] = await to(fetchUser(id));
if (!ok) {
// 这里 TypeScript 知道:
// - err 是 SafeError(非 null)
// - user 是 null
console.error(err.message);
return;
}
// 这里 TypeScript 知道:
// - err 是 null
// - user 是 User 类型(非 null)
console.log(user.name); // ✅ 不会报错
typescript
// TypeScript 类型收窄演示
const [ok, err, user] = await to(fetchUser(id));
// ^ok: boolean
// ^err: SafeError | null
// ^user: User | null
if (!ok) {
// ✅ 这个分支里 TypeScript 自动推断:
// err: SafeError (非 null,可以安全访问 .message)
// user: null
console.error(err.message); // ✅ 不报错
console.log(user.name); // ❌ 报错:user 是 null
return;
}
// ✅ 这个分支里 TypeScript 自动推断:
// err: null
// user: User (非 null,可以安全访问属性)
console.log(user.name); // ✅ 不报错
console.log(err.message); // ❌ 报错:err 是 null
不只是异步
同步代码也能用:
typescript
import { sync } from 'await-to-tuple';
const [ok, err, config] = sync(() => JSON.parse(jsonString));
if (!ok) {
console.error('JSON 解析失败:', err.message);
return getDefaultConfig();
}
return config;
还有一些实用工具
typescript
import { to, or, map, pipe } from 'await-to-tuple';
// 失败时用默认值
const name = or(await to(fetchName()), '匿名用户');
// 转换成功的结果
const upperName = map(await to(fetchName()), n => n.toUpperCase());
// 链式调用,遇到错误自动中断
const [ok, err, result] = await pipe(
userId,
fetchUser,
validateUser,
saveUser
);
和其他方案的对比
| 方案 | 优点 | 缺点 |
|---|---|---|
| try/catch | 原生支持 | 冗长、打断逻辑、类型不安全 |
| await-to-js | 简洁 | 二元组有歧义 |
| await-to-tuple | 三元组无歧义、类型安全、工具丰富 | 需要安装依赖 |
| TC39 try 操作符提案 | 语言原生 | 还在 Stage 1,遥遥无期 |
立即开始
安装
选择你喜欢的包管理器:
bash
# npm
npm install await-to-tuple
# pnpm(推荐)
pnpm add await-to-tuple
# yarn
yarn add await-to-tuple
基础用法
typescript
import { to, sync } from 'await-to-tuple';
// 异步操作
const [ok, err, data] = await to(fetch('/api/users'));
if (!ok) {
console.error('请求失败:', err.message);
return;
}
console.log(data);
// 同步操作
const [ok2, err2, json] = sync(() => JSON.parse(str));
if (!ok2) {
console.error('解析失败:', err2.message);
return;
}
console.log(json);
完整 API
typescript
import {
to, // 包装 Promise
sync, // 包装同步函数
or, // 获取值或默认值
map, // 转换成功结果
pipe, // 链式调用
} from 'await-to-tuple';
// 别名也可以用
import {
go, // = to
safeAwait, // = to
safeCall, // = sync
unwrapOr, // = or
safePipe, // = pipe
} from 'await-to-tuple';
零依赖,超轻量
- 📦 gzip 后 < 1KB
- 🌳 完全 Tree-shakeable
- 🔷 100% TypeScript,类型完备
- ✅ 完整测试覆盖
链接
- 📦 npm : www.npmjs.com/package/awa...
- 🐙 GitHub : github.com/asdzbb123/a...
写在最后
错误处理不应该是代码里最丑的部分。
await-to-tuple 不是什么革命性的东西,它只是把一个简单的想法------错误是值,不是异常------用最小的代码量实现出来。
如果你也受够了 try/catch,不妨试试。
typescript
const [ok, err, future] = await to(Promise.resolve('更好的代码'));
如果这篇文章对你有帮助:
- ⭐ 给个 Star:github.com/asdzbb123/a...
- 📦 npm 安装试试:
npm install await-to-tuple - 💬 有问题欢迎提 Issue
让我们一起告别 try/catch 地狱 🎉