Skip to content

🔥错误传播:如何让 Maybe 或 Either 链式处理失败?

——避免层层 if (result) return ...

“不要用 if 检查错误,
而要用类型系统让错误自动传播。”

一、问题:命令式错误处理的噩梦

传统写法:嵌套 if 检查

js
function getUserPreferences(userId) {
  const user = getUserById(userId);
  if (!user) return null;

  const profile = getProfile(user.profileId);
  if (!profile) return null;

  const prefs = getPreferences(profile.prefId);
  if (!prefs) return null;

  return enrichPreferences(prefs);
}
  • 重复模板代码
  • null 含义模糊(未找到?错误?)
  • 难以区分“正常空值”和“错误”
  • 无法携带错误信息

二、解决方案:Either 和 Maybe 类型

核心思想

类型表示“可能失败”的计算,
并通过 Monad 的 chain(flatMap) 自动传播错误。

三、Maybe:处理“存在性”问题

Maybe 是什么?

一个容器,表示值可能存在Just)或不存在Nothing)。

js
// 签名:
// getUserById :: Id -> Maybe User
// getProfile   :: ProfileId -> Maybe Profile
// getPrefs     :: PrefId -> Maybe Preferences

链式调用(自动传播)

js
const getUserPreferences = pipe(
  getUserById,          // Maybe User
  chain(user => getProfile(user.profileId)),     // Maybe Profile
  chain(profile => getPreferences(profile.prefId)), // Maybe Preferences
  map(enrichPreferences) // Maybe EnrichedPrefs
);
  • chain:仅在 Just 时执行下一步
  • map:转换值,失败则跳过
  • 无需 if,错误自动短路

Maybe 实现(简化版)

js
const Maybe = {
  of: value => Just(value),
  nothing: () => ({ 
    isNothing: true,
    map: () => Maybe.nothing(),
    chain: () => Maybe.nothing()
  })
};

const Just = value => ({
  isNothing: false,
  map: f => Just(f(value)),
  chain: f => f(value) // f 返回 Maybe
});

四、Either:处理“错误信息”问题

Either 是什么?

表示两种可能:Right(成功)或 Left(失败,含错误信息)。

js
// 签名:
// getUserById :: Id -> Either Error User
// getProfile   :: ProfileId -> Either Error Profile

链式调用(携带错误信息)

js
const getUserPreferences = pipe(
  getUserById,
  chain(user => user.active 
    ? Either.of(user) 
    : Left('User inactive')
  ),
  chain(user => getProfile(user.profileId)),
  chain(profile => getPreferences(profile.prefId)),
  map(enrichPreferences)
);

// 调用
const result = getUserPreferences(123);
if (result.isRight) {
  console.log(result.value);
} else {
  console.error(result.error); // "User inactive"
}
  • 失败时携带 Error 对象
  • 可用于日志、用户提示
  • 依然是链式,无 if

Either 实现(简化版)

js
const Right = value => ({
  isRight: true,
  map: f => Right(f(value)),
  chain: f => f(value)
});

const Left = error => ({
  isRight: false,
  map: () => Left(error),
  chain: () => Left(error)
});

const Either = {
  of: Right,
  left: Left
};

五、真实场景对比

命令式:脆弱且冗长

js
function processOrder(orderId) {
  const order = db.findOrder(orderId);
  if (!order) return { error: 'Order not found' };

  if (order.status !== 'pending') {
    return { error: 'Order not pending' };
  }

  const user = db.findUser(order.userId);
  if (!user) return { error: 'User not found' };

  const result = chargeUser(user, order.amount);
  if (!result.success) {
    return { error: result.message };
  }

  return { data: result };
}

函数式:清晰且健壮

js
const processOrder = pipe(
  db.findOrder,                    // Either NotFound Order
  chain(order => 
    order.status === 'pending'
      ? Either.of(order)
      : Left('Order not pending')
  ),
  chain(order => 
    db.findUser(order.userId)     // Either NotFound User
  ),
  chain(user => 
    chargeUser(user, order.amount) // Either ChargeError Result
  )
);
  • 所有步骤统一用 Either
  • 错误自动传播
  • 类型安全(在 TS 中)

六、工程实践建议

1. 在边界处转换

  • API 调用、数据库查询 → 返回 Either
  • UI 层 → 解构 Either,显示成功/错误
js
api.getUser(123).then(result => {
  if (result.isRight) {
    showUser(result.value);
  } else {
    showError(result.error.message);
  }
});

2. 用 fp-ts 或 crocks

ts
import * as E from 'fp-ts/Either';
import { pipe } from 'fp-ts/function';

const result = pipe(
  E.tryCatch(
    () => unsafeApiCall(),
    () => new Error('API failed')
  ),
  E.chain(validateResponse)
);

3. 避免过度使用

  • 简单逻辑无需 Either
  • 不要为了 FP 而 FP

七、总结:错误处理的范式转变

方式特点
命令式 if 检查手动传播,易遗漏,重复
异常 try/catch非表达式,难以组合,可能遗漏
Maybe/Either自动传播,类型安全,可组合

“让类型系统替你写 if。”

当你使用 Either 链:

  • 你不再“处理”错误,
  • 你定义“成功路径”,
  • 错误自动消失在 Left 中。

这才是优雅的错误传播。