在现代 Swift 架构(如 TCA、Redux 或 Combine 驱动的 MVVM)中,统一错误流的核心目标是:将不可控的运行时异常转换为可控的状态。
要实现这一点,我们需要在架构层建立一个“漏斗”,将分散的 throws 转换为统一的 Result,最后流入 Publisher(Combine)或 Effect(TCA)。
1. 架构分层:错误流的演变
在一个健壮的架构中,错误会经历从“动态”到“静态”的转变:
- 基础设施层 (IO/Network/Database) :使用
throws。因为这一层关注的是执行过程,报错是瞬时的。 - 仓库层 (Repository/Service) :捕获
throws并转换为Result<T, DomainError>。这是防御的核心,将底层错误映射为业务感知的错误。 - 表现层 (ViewModel/Reducer) :将
Result封装进Publisher或Effect。错误变成了**状态(State)**的一部分,驱动 UI 更新。
2. 统一错误定义:Domain Error
首先,不要在全架构范围内透传 Error 协议。定义每个模块特有的错误枚举:
Swift
enum SearchError: Error, Equatable {
case networkUnreachable
case invalidQuery
case serverError(code: Int)
case unknown(String)
}
3. 核心转换器:从 Throws 到 Result
利用 Result(catching:) 初始化器作为桥梁。在 Service 层统一拦截:
Swift
class SearchService {
// 内部使用 async throws 方便组合逻辑
private func rawSearch(query: String) async throws -> [ResultItem] { ... }
// 外部边界返回 Result,强制转换错误类型
func search(query: String) async -> Result<[ResultItem], SearchError> {
do {
let items = try await rawSearch(query: query)
return .success(items)
} catch let error as SearchError {
return .failure(error)
} catch {
return .failure(.unknown(error.localizedDescription))
}
}
}
4. 结合 Combine Publisher / TCA Effect
一旦错误被规范化为 Result,进入响应式框架就变得非常顺滑。
A. 在 Combine 中统一
通过 Result 构造 Publisher,可以确保错误流的类型安全性,避免 Subscribers.Completion.failure 携带模糊的 Error。
Swift
func searchPublisher(query: String) -> AnyPublisher<[ResultItem], SearchError> {
Future { promise in
Task {
let result = await self.service.search(query: query)
promise(result) // 直接传递 Result
}
}
.eraseToAnyPublisher()
}
B. 在 TCA (The Composable Architecture) 中统一
在 TCA 中,Effect 实际上是返回一个 Action。利用 catchToEffect(或 Swift 6 的原生转换)将错误流转为 Action。
Swift
// Reducer 内部处理
case .searchButtonTapped:
state.isLoading = true
return .run { [query = state.query] send in
// 执行 Effect 并捕获结果
let result = await service.search(query: query)
await send(.searchResponse(result))
}
case let .searchResponse(.failure(error)):
state.isLoading = false
state.errorMessage = error.localizedDescription
return .none
5. 架构统一的收益:防御式链路
- 编译期安全:通过
Result<T, SearchError>,编译器会强制你在switch中处理所有可能的业务错误,避免了do-catch遗漏特定分支的风险。 - 副作用纯净化:
Effect将网络请求这种不确定的副作用变成了确定的Action。这意味着你的业务逻辑(Reducer)是纯函数,极易测试。 - 统一上报:可以在
Effect的基类或拦截器(Middleware)中统一捕获DomainError进行埋点上报,而无需在每个 API 调用处写重复代码。
总结:错误流转公式
throws(基础层)Result(业务层)Action(表现层)State(UI 层)
你想了解如何编写一个通用的“错误转换中间件”,自动将底层的 URLError 或 DecodingError 映射到你的业务枚举中,从而彻底消除 Service 层的冗余 do-catch 吗?