11-5.【错误处理】throws 对异步函数 (async throws) 有哪些特殊注意事项?

2 阅读3分钟

在 Swift Concurrency(async/await)模型中,async throws 的组合并不是简单的“异步 + 错误处理”的叠加,而是在控制流、性能以及任务生命周期上引入了新的规则。

以下是设计和使用 async throws 时的核心注意事项:


1. 错误传播与任务取消 (Cancellation)

在异步函数中,错误抛出通常与任务取消交织在一起。

  • 隐式检查:当一个 async throws 函数内部抛出错误时,它会自动中止当前任务。但更重要的是,如果外部取消了任务,许多系统 API(如 Task.checkCancellation())会主动抛出 CancellationError
  • 注意事项:在编写长耗时的异步循环时,你应该防御性地检查取消状态。如果任务已取消,抛出错误是终止执行的最佳方式。

Swift

func processLargeData() async throws {
    for item in data {
        // 防御式编程:在继续之前检查任务是否还合法
        try Task.checkCancellation() 
        try await process(item)
    }
}

2. 堆栈回溯与“挂起点”

与同步 throws 不同,async throws 的执行可能会跨越多个线程。

  • 挂起点(Suspension Points) :每一个 try await 都是一个潜在的挂起点。
  • 性能影响:在同步代码中,throws 只是简单的寄存器跳转。而在异步代码中,如果 await 的函数抛出错误,运行时需要恢复之前保存的异步上下文(Continuation) ,并将错误注入该上下文。这比同步抛出稍微昂贵一些,因为它涉及状态机的切换。

3. 避免“双重失败”陷阱

在处理异步任务组(Task Groups)时,错误处理变得复杂:

  • 首位获胜原则:在一个 ThrowingTaskGroup 中,如果其中一个子任务抛出错误,整个 Group 会被标记为失败。
  • 清理责任:即使一个子任务抛出错误,其他已经在运行的子任务不会立即停止,除非你在代码中显式检查取消。

Swift

try await withThrowingTaskGroup(of: Void.self) { group in
    group.addTask { try await task1() }
    group.addTask { try await task2() }
    
    // 如果 task1 崩了,task2 依然在后台跑,除非你调用了 Task.checkCancellation()
    try await group.waitForAll()
}

4. 结果包装:TaskResult 的转换

当你从一个非异步环境启动一个异步任务时,错误处理的边界会发生变化:

  • Task 句柄let task = Task { try await fetchData() }。此时,错误被封装在 task.result 中(类型为 Result<Success, Error>)。
  • 注意事项:如果你忘记在读取 task.value 时使用 try,或者没有处理 task.result,错误就会被静默忽略。这是异步防御式编程中的常见漏洞。

5. 类型化抛出 (Typed Throws) 的新优势

在 Swift 6.0+ 中,async throws(MyError) 对异步函数尤为重要:

  • 泛型约束:在处理复杂的异步流(AsyncSequence)时,明确的错误类型可以让编译器验证你的 catch 块是否覆盖了所有可能的异步异常。
  • 性能优化:减少了异步上下文恢复时对 any Error 进行动态拆箱的开销。

总结:async throws 最佳实践清单

  1. 始终检查取消:在 async throws 循环内部调用 try Task.checkCancellation()
  2. 优雅降级:在 await 之后,考虑是否可以用 try??? 快速提供默认值,以避免不必要的错误冒泡导致整个任务链崩溃。
  3. 明确清理:利用 defer 块确保即使异步函数抛出错误,资源(如数据库连接、文件句柄)也能被正确关闭。
  4. 优先使用 async/await 而非 Result 闭包:它消除了回调地狱中的“忘记调用 completion”导致的错误丢失风险。