Swift 错误模型
Swift 的错误处理三件套:Error 协议定义错误类型、throws 标记可能失败的函数、do-catch 负责接住错误。编译器强制处理,遗漏比 Java 的 checked exception 更隐蔽——不 catch 编译直接拒绝。
错误不是异常,是控制流的一部分。
先自定义一个符合 Error 的枚举,把业务错误归类:参数错、网络错、解析错、未知错。分得越细,调用方越好针对处理。
do-catch 与 throws
函数体里 throw 抛出错误,调用方用 do-catch 接。catch 可以按类型分支,也可以兜底。想继续往上抛就写 throws 不 catch。
enum ApiError: Error {
case invalidURL
case network(Int)
case decodeFailed
}
func fetchUser(id: Int) throws -> User {
guard id > 0 else { throw ApiError.invalidURL }
let data = try loadData(id: id)
return try decode(data)
}
do {
let user = try fetchUser(id: 42)
render(user)
} catch ApiError.invalidURL {
showMessage("参数错误")
} catch ApiError.network(let code) {
showMessage("网络错误:\(code)")
} catch {
showMessage("未知错误")
}
try 有三种写法:try 原样传播、try? 结果变可选、try! 强制(几乎别用)。每层函数都要想清楚:错误该在这层消化,还是继续往上抛。
Result 与错误转换
Result 把成功值和错误包在一个值里:.success(value) 或 .failure(error)。异步回调、Combine、任务队列里特别顺手,不用到处写 throws。
func loadProfile(_ completion: @escaping (Result<Profile, ApiError>) -> Void) {
URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
completion(.failure(.network(error.code)))
return
}
guard let data = data,
let profile = try? JSONDecoder().decode(Profile.self, from: data) else {
completion(.failure(.decodeFailed))
return
}
completion(.success(profile))
}.resume()
}
Result 和 throws 可以互转:Result { try risky() } 把 throws 包进 Result,get() 再把 Result 变回 throws。边界处用 Result 收口,内层尽情 throws,分工清晰。
重试与降级策略
网络抖动是常态,瞬时失败值得重试,业务性失败重试也没用。重试要带退避:第一次等 0.5 秒,第二次 1 秒,最多三次。降级则是给用户一个次优但可用的结果。
func fetchWithRetry(maxAttempts: Int = 3) async throws -> Data {
var attempt = 0
var lastError: Error?
while attempt < maxAttempts {
do {
return try await fetch()
} catch {
lastError = error
attempt += 1
let delay = Double(attempt) * 0.5
try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
}
}
throw lastError!
}
降级举例:详情图加载失败用本地占位图、推荐接口超时返回缓存、头像解析失败显示首字母。用户不该因为一个非核心功能失败就看着白屏。
重试与降级之外,错误处理还有一个容易忽略的原则:把错误边界画清楚。哪些错误属于调用方可以处理的业务错误,哪些属于内部不可恢复的系统错误,要在设计阶段就明确下来。可恢复的错误向上抛出让上层决定如何应对,不可恢复的错误就地记录并快速失败,避免错误被层层包装后丢失原始上下文。给错误附加信息时,尽量保留底层的错误码和失败原因,排查线上问题时,一个带完整链路的日志往往比堆叠的兜底代码更有价值。
日志与监控
错误处理做到位还不够,得能看见。统一在出口记录日志:错误类型、上下文、堆栈,带上用户可读的描述。线上监控把这些错误汇聚起来,崩溃之外的「软失败」也能被发现。
func logError(_ error: Error, context: String) {
let message = "[\(context)] \(error.localizedDescription)"
print(message)
// 上报到监控平台,聚合同类错误计数
}
记录时把上下文带上:哪个页面、哪个接口、什么参数。没上下文的错误日志等于没记。分级也重要:可恢复的记警告,致命的上报崩溃。
实战:网络层错误处理
把整套策略装进一个网络请求:校验状态码、区分可重试错误、超时设置、结果统一转成业务错误。调用方永远面对一个干净的 API。
struct APIClient {
func request<T: Decodable>(_ type: T.Type, path: String) async throws -> T {
guard let url = URL(string: baseURL + path) else { throw ApiError.invalidURL }
let (data, response) = try await session.data(from: url)
guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode) else {
throw ApiError.network((response as? HTTPURLResponse)?.statusCode ?? -1)
}
do {
return try decoder.decode(T.self, from: data)
} catch {
throw ApiError.decodeFailed
}
}
}
UI 层只面对三类结果:有数据、明确错误提示、可重试引导。业务错误码映射成用户语言,技术错误进日志,用户永远看到的是能行动的提示。
小结
Swift 错误处理闭环:Error 枚举分类、throws 传播、do-catch 消化、Result 收口异步、重试带退避、降级保体验、日志看全局。记住三个原则:错误要分类、边界要收口、出口要记录。别把错误处理当补丁,它是健壮性的骨架。代码写得好不好,很多时候就看它面对失败时有没有尊严。