11-10.【错误处理】Result 与 async/await 配合使用时的设计注意点。

0 阅读2分钟

在 Swift 现代并发(Swift Concurrency)体系下,async/await 已经成为了处理异步流的主流方式。然而,Result 并没有退场,它从“流程控制工具”转型为了**“状态保存工具”**。

在架构设计中配合使用这两者时,需要注意以下几个核心点,以保持代码的健壮性:


1. 明确分工:throws 负责流转,Result 负责存储

这是最基本的设计原则。

  • 执行层(Executing) :函数应当声明为 async throws。这使得你可以利用 await 语法的简洁性,并让错误通过 Swift 运行时的异常机制自动传播。
  • 持有层(Holding) :当异步操作完成,你需要将结果存入 ViewModelState 供 UI 消费时,将其包装为 Result

2. 避免在 Task 闭包中丢失错误

当你从同步环境进入异步环境时,Task 会隐式地捕获错误。如果你不小心,错误可能会被“吞掉”。

Swift

// ⚠️ 危险做法:错误在 Task 内部发生,外部无法得知
Task {
    let user = try await service.fetchUser()
    self.user = user
}

// ✅ 防御式做法:将结果捕获为 Result 并显式处理
Task {
    let result = await Result { try await service.fetchUser() }
    await MainActor.run {
        self.loadingResult = result // 将 Result 存入状态,驱动 UI
    }
}

3. 利用 Result 处理“非阻塞”的并发任务

如果你发起多个独立的异步请求,且不希望其中一个失败就导致整个流程崩塌(async letTaskGroup 默认的短路行为),可以先将每个任务的结果包装为 Result

Swift

async let userResult = Result { try await fetchUser() }
async let postsResult = Result { try await fetchPosts() }

// 这里不会因为任何一个请求失败而抛出,你可以分别处理成功和失败的部分
let user = try? (await userResult).get()
let posts = (try? (await postsResult).get()) ?? []

4. 类型化错误(Typed Throws)的衔接

Swift 6 引入了 Typed Throws。在设计 Resultasync 配合的 API 时,利用明确的错误类型可以极大增强防御性。

Swift

// 这种定义让编译器保证了:要么得到 User,要么得到特定的 APIError
func loadUser() async -> Result<User, APIError> {
    do {
        let user = try await fetchRemoteUser()
        return .success(user)
    } catch let error as APIError {
        return .failure(error)
    } catch {
        return .failure(.unknown)
    }
}

5. 状态机设计:从 Loading 到 Result

在结合 SwiftUI 或异步 UI 时,Result 是定义状态机的完美工具。配合 async/await,你可以消除复杂的布尔逻辑(如 isLoading, hasError)。

UI 状态实现方式
初始/加载中nil (Optional Result)
执行成功.success(data)
执行失败.failure(error)

总结:防御式组合建议

  1. 内部 API:优先使用 async throws,保持代码扁平化,减少嵌套。
  2. 跨组件/状态存储:使用 Result。它提供了一个稳定的、可传递的快照,反映了异步任务在某一时刻的确切结局。
  3. 桥接技巧:熟练使用 Result(catching:) 初始化器将异步抛出转换为值,以及使用 result.get() 将保存的值重新投入抛出链条中。