告别 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);
}

看着这坨代码,我问自己:这真的是 2025 年该有的写法吗?

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('更好的代码'));

如果这篇文章对你有帮助:

让我们一起告别 try/catch 地狱 🎉

相关推荐
UIUV2 小时前
JavaScript中this指向机制与异步回调解决方案详解
前端·javascript·代码规范
momo1002 小时前
IndexedDB 实战:封装一个通用工具类,搞定所有本地存储需求
前端·javascript
liuniansilence2 小时前
🚀 高并发场景下的救星:BullMQ如何实现智能流量削峰填谷
前端·分布式·消息队列
再花2 小时前
在Angular中实现基于nz-calendar的日历甘特图
前端·angular.js
星辰烈龙2 小时前
黑马程序员Java基础9
java·开发语言
San302 小时前
从零到一:彻底搞定面试高频算法——“列表转树”与“爬楼梯”全解析
javascript·算法·面试
GISer_Jing2 小时前
今天看了京东零售JDS的保温直播,秋招,好像真的结束了,接下来就是论文+工作了!!!加油干论文,学&分享技术
前端·零售
Mapmost2 小时前
【高斯泼溅】如何将“歪头”的3DGS模型精准“钉”在地图上,杜绝后续误差?
前端
@游子2 小时前
Python类属性与魔术方法全解析
开发语言·python