告别 throw exception!为什么 Result<T> 才是业务逻辑的正确选择

3 阅读3分钟

告别 throw exception!为什么 Result<T> 才是业务逻辑的正确选择

在传统面向对象编程中,异常(Exception) 被广泛用于处理错误——比如用户不存在、余额不足、参数非法等。但随着系统复杂度提升,我们逐渐发现:用异常表达业务失败,是一种反模式

真正健壮、可维护、可测试的业务代码,应该使用 Result<T>(或 Either<Error, T> 这样的显式返回类型来表达“成功”与“失败”的两种可能。


❌ 为什么 throw 在业务逻辑中是个坏主意?

1. 控制流隐藏,破坏可读性

// 看似简单,但内部可能 throw 十几种异常
var order = CreateOrder(userId, items);
ProcessPayment(order);

你无法从方法签名知道它会抛什么异常,必须阅读源码或文档。这违反了显式优于隐式原则。

2. 异常 ≠ 异常情况

“异常”应表示程序无法继续的意外状态(如数据库宕机、网络中断),而非可预期的业务规则失败(如“库存不足”)。

把“库存不足”当作异常,等于把正常业务分支当作系统故障处理,混淆了语义层次。

3. 性能开销大

在 .NET、Java 等运行时中,抛出异常会捕获完整的调用栈,成本高昂。若高频业务路径(如登录失败)依赖异常,将拖慢系统。

4. 难以组合与链式处理

你想连续执行多个可能失败的操作?用异常只能嵌套 try-catch,代码迅速变得臃肿:

try {
    var user = GetUser(id);
    try {
        var cart = GetCart(user);
        try {
            PlaceOrder(cart);
        } catch (OutOfStockException) { ... }
    } catch (CartEmptyException) { ... }
} catch (UserNotFoundException) { ... }

Result<T>:让失败成为一等公民

Result<T> 是一个泛型类型,通常定义为:

public class Result<T>
{
    public bool IsSuccess { get; }
    public T Value { get; }          // 成功时有效
    public Error Error { get; }      // 失败时有效
}

或者使用更函数式的 Either<Error, T>

优势一:签名即契约

Result<Order> CreateOrder(UserId userId, List<Item> items);

一眼看出:这个方法可能成功(返回 Order),也可能失败(返回 Error)。无需看文档,无需猜异常

优势二:支持链式操作(Railway-Oriented Programming)

借助 MapBindMatch 等方法,可以优雅地串联多个可能失败的操作:

return GetUser(userId)
    .Bind(user => GetCart(user))
    .Bind(cart => ValidateInventory(cart))
    .Bind(validCart => PlaceOrder(validCart));

一旦某一步失败,后续自动跳过,最终返回第一个错误。像铁路轨道一样,成功走“主轨”,失败走“侧轨”

优势三:错误类型可枚举、可测试

你可以定义明确的错误类型:

public record UserNotFound(UserId Id) : Error;
public record InsufficientBalance(decimal Balance, decimal Required) : Error;

单元测试时,直接断言 result.Error is InsufficientBalance,精准验证业务逻辑。

优势四:与 API 层无缝集成

在 Web API 中,只需一个统一的 Result 到 HTTP 响应的转换器:

if (result.IsSuccess)
    return Ok(result.Value);
else
    return BadRequest(result.Error.Message);

避免到处写 try-catch 返回不同状态码。


🛠 实践建议

  • 区分两类错误

    • 系统异常(如 DB 连接失败)→ 仍用 throw,由全局异常处理器兜底。
    • 业务失败(如“优惠券已过期”)→ 用 Result<T> 显式返回。
  • 不要滥用 Result 包装所有方法,仅用于有明确业务失败场景的逻辑。

  • 语言支持:C# 可用 FluentResultsOneOf;Rust 的 Result<T, E> 是语言原生;TypeScript 可用 neverthrow 库。


💡 结语

异常是控制流的 goto,而 Result<T> 是结构化错误处理的 if-else。

在业务逻辑中,失败不是“意外”,而是预期的一部分。用 Result<T> 显式建模这种不确定性,不仅让代码更健壮、可读、可测,也迫使开发者认真思考每一种失败场景——这才是专业软件工程的体现。

是时候,告别 throw new BusinessException() 了。


🌟 记住:好的代码,不靠异常讲故事;好的架构,让失败也体面。