告别 try/catch 地狱:用三元组重新定义 JavaScript 错误处理

告别 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 里的 eunknown,每次都要手动判断类型。

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,类型完备
  • ✅ 完整测试覆盖

链接


写在最后

错误处理不应该是代码里最丑的部分。

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 地狱 🎉

相关推荐
一念之间lq5 小时前
Elpis 第三阶段· 领域模型架构建设
前端·后端
哆啦A梦15885 小时前
商城后台管理系统 01 Vue-i18n国际化
前端·javascript·vue.js
期待のcode5 小时前
Vue的安装创建与运行
前端·javascript·vue.js
百锦再5 小时前
国产数据库的平替亮点——关系型数据库架构适配
android·java·前端·数据库·sql·算法·数据库架构
旺仔Sec5 小时前
2025年海南省职业院校技能大赛“应用软件系统开发“赛项竞赛样题
前端·应用软件系统开发
FakeOccupational6 小时前
【树莓派 002】 RP2040 实现示波器 PIO来驱动 ADC10080 并抓取数据方案+ 内置12-bitADC&DMA&网页前端可视化方案
前端
至善迎风6 小时前
Bun:下一代 JavaScript 运行时与工具链
开发语言·javascript·ecmascript·bun
DJ斯特拉6 小时前
Vue工程化
前端·javascript·vue.js