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!
}

降级举例:详情图加载失败用本地占位图、推荐接口超时返回缓存、头像解析失败显示首字母。用户不该因为一个非核心功能失败就看着白屏。

重试与降级之外,错误处理还有一个容易忽略的原则:把错误边界画清楚。哪些错误属于调用方可以处理的业务错误,哪些属于内部不可恢复的系统错误,要在设计阶段就明确下来。可恢复的错误向上抛出让上层决定如何应对,不可恢复的错误就地记录并快速失败,避免错误被层层包装后丢失原始上下文。给错误附加信息时,尽量保留底层的错误码和失败原因,排查线上问题时,一个带完整链路的日志往往比堆叠的兜底代码更有价值。