在 Combine 的错误处理体系中,.catch 和 .tryCatch 是防范流因错误而“猝死”的两道核心防线。它们最本质的区别在于:处理闭包本身是否具备“抛出错误”的能力,以及它们对下游 Failure 类型的影响。
1. .catch:平滑的避风港
本质:当上游发生错误时,拦截该错误,并替换为一个新的 Publisher。
-
核心逻辑:上游一旦报错,
.catch就像一个接力棒,让流转入一个新的分支。原来的上游流会因为错误而终止,但下游会继续接收新 Publisher 的值。 -
类型约束:新返回的 Publisher 的
Output和Failure类型必须与原链条完全一致。 -
典型场景:
- 备选方案:主接口请求失败,通过
.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. 防御式编程技巧:如何选择?
-
如果你只想“息事宁人” :
使用
.catch { _ in Just(defaultValue) }。这常用于 UI 绑定,防止一个网络错误导致 UI 组件永远不再响应更新。 -
如果你需要“升级处理” :
使用
.tryCatch。例如,当检测到 401 错误时,尝试刷新 Token,如果刷新也失败,则抛出authExpired给全局处理。 -
如果不想丢失类型信息:
坚持使用
.catch。如果你需要根据错误类型返回不同 Publisher,可以使用switch配合不同的Just或Fail返回。
总结
.catch是“我来搞定,大家继续”,它是确定性的。.tryCatch是“我先看看,不行我还得报个警”,它是不确定性的。