11-9.【错误处理】如何在架构层统一错误流,结合 Result 和 Publisher/Effect?

7 阅读3分钟

在现代 Swift 架构(如 TCA、Redux 或 Combine 驱动的 MVVM)中,统一错误流的核心目标是:将不可控的运行时异常转换为可控的状态。

要实现这一点,我们需要在架构层建立一个“漏斗”,将分散的 throws 转换为统一的 Result,最后流入 Publisher(Combine)或 Effect(TCA)。


1. 架构分层:错误流的演变

在一个健壮的架构中,错误会经历从“动态”到“静态”的转变:

  1. 基础设施层 (IO/Network/Database) :使用 throws。因为这一层关注的是执行过程,报错是瞬时的。
  2. 仓库层 (Repository/Service) :捕获 throws 并转换为 Result<T, DomainError>。这是防御的核心,将底层错误映射为业务感知的错误。
  3. 表现层 (ViewModel/Reducer) :将 Result 封装进 PublisherEffect。错误变成了**状态(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 (基础层) \rightarrow Result (业务层) \rightarrow Action (表现层) \rightarrow State (UI 层)

你想了解如何编写一个通用的“错误转换中间件”,自动将底层的 URLErrorDecodingError 映射到你的业务枚举中,从而彻底消除 Service 层的冗余 do-catch 吗?