导读:本期聚焦于张立峰创作的《Swift async/await 并发下 try?、try! 和 rethrows 分别有哪些坑?》,敬请观看详情。把 try? 直接搬到 async 函数里,以为错误只是变成 nil,结果在网络请求失败后界面卡在加载状态,这种问题在 Swift 并发代码中并不少见。async/await 让错误路径从回调堆栈变成了线性代码,但 try?、try! 和 rethrows 的语义并没有因此简化。try? 会静默吞掉所有错误,包括 CancellationError,这在任务取消时可能隐藏关键状态;try! 一旦遇到错误会让整个任务崩溃,而且并发任务的崩溃点往往不在调用处;rethrows 则在高阶异步函数中决定错误是否向上传递,写错签名会导致编译器无法推断或错误被意外吞噬。本文围绕这三个关键字在 async let、TaskGroup、withThrowingTaskGroup 等场景下的表现展开,结合代码说明如何避免静默失败、正确识别重抛边界,并选择合适的错误处理策略。

Swift 引入 async/await 后,错误处理代码从嵌套闭包中解放出来,但 try 关键字家族在并发挂起点前后的行为仍有一些容易忽视的差别。try? 会把错误压成 nil,try! 会在遇到错误时直接触发 fatalError,而 rethrows 则控制高阶函数中错误是否向上游传递。三者放在 Task、async let、TaskGroup 等并发结构里,如果只按同步代码的习惯理解,往往会出现静默失败、延迟崩溃或错误类型被吞掉的问题。

Swift async/await 并发下 try?、try! 和 rethrows 分别有哪些坑?

一、async/await 下三种 try 的基础语义

同步代码中,try 要求调用者要么用 do-catch 捕获,要么继续把错误向上抛。async throws 函数的调用点必须同时出现 try 和 await,顺序上 try await 是不可拆分的语法组合。try? 会把错误扁平化为可选值:成功时返回 .some,失败时返回 nil,不再保留具体 Error 类型。try! 则放弃所有错误传播,一旦抛出错误就触发运行时 fatalError。这些基本规则并不会因为并发而改变,但并发会放大它们的副作用。

以网络请求为例,下面的 fetchUser 函数会因状态码、解码或网络问题抛出不同类型错误。使用 try? await 调用后,URLSession 的 URLError、自定义 APIError 以及任务取消产生的 CancellationError 全部被压成一个 nil。调用方无法区分究竟是数据不存在、网络断开还是整个任务已经被取消。

import Foundation

enum APIError: Error {
    case invalidEncoding
    case badStatus(Int)
}

func fetchUser() async throws -> String {
    let url = URL(string: "https://ipipp.com/user")!
    let (data, _) = try await URLSession.shared.data(from: url)
    guard let name = String(data: data, encoding: .utf8) else {
        throw APIError.invalidEncoding
    }
    return name
}

func downloadAvatar() async throws -> Data {
    let url = URL(string: "https://ipipp.com/avatar")!
    let (data, _) = try await URLSession.shared.data(from: url)
    return data
}

这个例子中,try? await fetchUser() 返回 String?。如果只是想做预加载缓存,nil 可以接受;但若是页面关键数据,nil 会导致界面卡在空状态,而且问题难以复盘。

二、try? 在并发场景中的静默失败风险

并发任务取消是 Swift 结构化并发的重要机制。Task 被取消时,正在执行的异步函数通常会在下一个挂起点抛出 CancellationError。如果调用链上充斥着 try?,这个取消错误就会被吞掉,任务看起来正常返回了 nil,但实际上协作取消流程被打断。例如用户离开页面后,后台任务本应提前结束,却可能继续执行耗时操作。

更典型的场景是 async let 并行加载多个资源。下面代码同时请求用户名和头像,两个子任务都使用 try? 包装。即使网络请求全部失败,userName 和 imageData 也只是 nil,外层逻辑没有任何错误提示。这种写法会让业务层把请求失败和正常空值混为一谈。

func loadUserData() async {
    async let name = try? fetchUser()
    async let avatar = try? downloadAvatar()
    let userName = await name
    let imageData = await avatar
    // userName 与 imageData 都可能为 nil
}

在 TaskGroup 中问题会更隐蔽。多个子任务并发执行,只要有一个失败,整体结果可能应该标记为失败,但如果每个子任务都返回 Optional,父任务就只能拿到一个装满 nil 的数组,连哪个键对应的操作失败都不知道。此时更推荐使用 withThrowingTaskGroup 保留错误抛出能力,在子任务内部不吞错,而由父任务统一捕获。

三、try! 的延迟崩溃与任务隔离问题

try! 表示调用者断言这个操作绝对不会失败。同步代码中如果断言错误,崩溃会立即发生在 try! 所在行,调试相对直接。但在 async/await 中,try! await 可能在一个独立 Task 或 actor 隔离域中执行。实际抛出错误的调用栈与业务代码创建任务的位置相距很远,崩溃信息里常常只显示 Swift 运行时的 fatalError,而不是最初触发网络请求的函数名。

并发任务具有隔离边界,try! 造成的进程崩溃不能像同步异常那样被上层 do-catch 拦截。即使你在 Task 外层写了错误处理,也无法捕获任务内部的 fatalError。这意味着一个偶发的服务端错误、一次网络抖动或返回数据格式变化,都可能直接杀死整个 App。

func loadProfile() {
    Task {
        let name = try! await fetchUser()
        print(name)
    }
}

这段代码看起来简洁,但在生产环境中极其危险。只要 fetchUser 抛出一个错误,Task 所在的线程就会触发 fatalError,而且这个错误不会变成 Swift 错误返回给 loadProfile。更好的做法是让 Task 继承外层作用域的错误处理,或者用 do-catch 记录错误后降级显示占位内容。

四、rethrows 在异步高阶函数中的传播边界

rethrows 的作用是让函数具备条件抛出能力:函数本身不主动 throw,但当传入的闭包参数抛出错误时,函数也会抛出该错误。这个关键字在同步高阶函数中很常见,进入 async/await 之后,其规则向异步闭包自然延伸。异步 rethrows 函数要求闭包参数是 async throws,函数体内部使用 try await 调用闭包,并且不能额外抛出闭包错误之外的新错误。

下面实现一个 mapAsync 函数,对数组元素逐个执行异步变换。由于 transform 可能抛错,函数不能写成普通 async;但又不想让所有调用者都被迫处理错误,因此 rethrows 是最合适的签名。

func mapAsync<T, U>(_ items: [T], transform: (T) async throws -> U) async rethrows -> [U] {
    var output: [U] = []
    for item in items {
        output.append(try await transform(item))
    }
    return output
}

如果调用 mapAsync 时传入的 transform 是 async 非 throws 闭包,那么整个 mapAsync 调用不需要 try;如果 transform 是 async throws,则调用端必须使用 try await。这种条件传播能极大提高 API 的易用性,避免无意义的 do-catch 包裹。

Swift 标准库中的 withThrowingTaskGroup 也遵循类似思路。它的闭包参数是 async throws,函数自身标记 rethrows,因此当闭包内部没有错误抛出时,调用方无需处理错误;一旦某个子任务失败,错误会沿任务组边界重新抛给调用者。自定义并发工具函数时,优先考虑 rethrows 可以避免错误语义被过早抹平。

五、并发错误处理策略建议

在实际并发代码中,三种 try 写法的选择应该遵循一个简单原则:错误如果不影响主流程,才能用 try?;确定不可能失败,才用 try!;而凡是高阶函数需要转发闭包错误,优先 rethrows。对于网络请求、数据库访问、文件 I/O 这类典型会失败的异步操作,try? 基本不应作为默认选项,因为它把错误信息完全丢弃。

更稳妥的方式是使用 do-catch 捕获 try await,并在 catch 中优先判断 Task.isCancelled 或错误类型是否为 CancellationError。这样既能响应结构化并发取消,也能区分真正的业务失败。对于需要继续执行的部分,可以采用 Result 类型或自定义错误枚举把失败信息显式保留,再由上层统一处理。

最后要注意任务组的错误边界。withThrowingTaskGroup 会等待所有子任务完成后统一重抛,适合整组操作需要原子性的场景;如果某个子任务失败后允许其他任务继续,则应在子任务内部捕获错误并记录日志,但要避免使用 try? 静默吞掉 CancellationError。并发错误处理的核心不是在每一层都消除错误,而是让错误在正确的边界出现。

Swift async/await错误处理rethrows修改时间:2026-10-02 23:21:31

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/1002/64859.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。