13-12.【Combine】catch 与 tryCatch 的差异及对错误传播的影响?

4 阅读3分钟

在 Combine 的错误处理体系中,.catch.tryCatch 是防范流因错误而“猝死”的两道核心防线。它们最本质的区别在于:处理闭包本身是否具备“抛出错误”的能力,以及它们对下游 Failure 类型的影响。


1. .catch:平滑的避风港

本质:当上游发生错误时,拦截该错误,并替换为一个新的 Publisher。

  • 核心逻辑:上游一旦报错,.catch 就像一个接力棒,让流转入一个新的分支。原来的上游流会因为错误而终止,但下游会继续接收新 Publisher 的值。

  • 类型约束:新返回的 Publisher 的 OutputFailure 类型必须与原链条完全一致

  • 典型场景

    • 备选方案:主接口请求失败,通过 .catch 切换到读取本地缓存。
    • 错误静默:报错时返回一个 Just(defaultValue),将错误类型抹除为 Never

Swift

fetchRemoteData()
    .catch { error in
        // 必须返回一个 Publisher
        return fetchLocalCache() 
    }

2. .tryCatch:具有“二次杀伤力”的检查站

本质:在上游报错时,它不仅能拦截,还允许你在处理逻辑中**再次抛出(throw)**新的错误。

  • 核心逻辑:它的闭包是 throws 的。这意味着你可以检查上游的错误,如果发现该错误无法修复,你可以直接抛出一个自定义的业务错误。

  • 类型影响:一旦使用 tryCatch,下游的 Failure 类型会自动被抹除为通用的 Error(因为 Swift 的 throws 无法在编译期约束具体类型)。

  • 典型场景

    • 错误重塑:将底层的 URLError 拦截,通过判断状态码,抛出一个更具业务意义的 AppError.invalidToken

3. 核心差异对比表

特性.catch.tryCatch
闭包签名(Failure) -> Publisher(Failure) throws -> Publisher
二次报错不允许在闭包内 throw允许再次抛出错误
下游 Failure 类型保持不变变为通用的 Error
恢复机制必须提供一个“接班”的流可以提供接班流,也可以直接中断并报错

4. 对错误传播的深远影响

A. 链条的终结性

无论是 catch 还是 tryCatch一旦进入它们的闭包,意味着原本的上游流已经彻底结束了。它们返回的是“新流”。如果你希望上游在报错后还能继续(例如:用户输错一次密码后还能继续输入),你不能在主链条上直接用 catch,而必须在 flatMap 内部的子流中使用。

B. 类型安全性的丧失

使用 tryCatch 或任何带 try 前缀的操作符(如 tryMap)都会导致类型丢失。如果你后续需要精确判断错误类型,你必须在链条末尾使用 .mapError 将其转回具体的枚举,或者在 sink 中进行强制类型转换。


5. 防御式编程技巧:如何选择?

  1. 如果你只想“息事宁人”

    使用 .catch { _ in Just(defaultValue) }。这常用于 UI 绑定,防止一个网络错误导致 UI 组件永远不再响应更新。

  2. 如果你需要“升级处理”

    使用 .tryCatch。例如,当检测到 401 错误时,尝试刷新 Token,如果刷新也失败,则抛出 authExpired 给全局处理。

  3. 如果不想丢失类型信息

    坚持使用 .catch。如果你需要根据错误类型返回不同 Publisher,可以使用 switch 配合不同的 JustFail 返回。


总结

  • .catch 是“我来搞定,大家继续”,它是确定性的。
  • .tryCatch 是“我先看看,不行我还得报个警”,它是不确定性的。