error-handling-patterns

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Error Handling Patterns — Expert Decisions

错误处理模式——专家决策

Expert decision frameworks for error handling choices. Claude knows Swift error syntax — this skill provides judgment calls for error type design and recovery strategies.

错误处理选择的专家决策框架。Claude 熟悉 Swift 错误语法——本技能为错误类型设计和恢复策略提供判断依据。

Decision Trees

决策树

throws vs Result

throws vs Result

Does the caller need to handle success/failure explicitly?
├─ YES (caller must acknowledge failure)
│  └─ Is the failure common and expected?
│     ├─ YES → Result<T, E> (explicit handling, no try/catch)
│     └─ NO → throws (exceptional case)
└─ NO (caller can ignore failure)
   └─ Use throws with try? at call site
When Result wins: Parsing, validation, operations where failure is common and needs explicit handling. Forces exhaustive switch.
When throws wins: Network calls, file operations where failure is exceptional. Cleaner syntax with async/await.
调用者是否需要显式处理成功/失败?
├─ 是(调用者必须确认失败情况)
│  └─ 失败是否常见且可预期?
│     ├─ 是 → Result<T, E>(显式处理,无需 try/catch)
│     └─ 否 → throws(异常情况)
└─ 否(调用者可忽略失败)
   └─ 在调用点使用 try? 搭配 throws
Result 的适用场景:解析、验证等失败常见且需要显式处理的操作。强制要求穷举 switch 处理。
throws 的适用场景:网络请求、文件操作等失败属于异常情况的场景。搭配 async/await 语法更简洁。

Error Type Granularity

错误类型粒度

How specific should error types be?
├─ API boundary (framework, library)
│  └─ Coarse-grained domain errors
│     enum NetworkError: Error { case timeout, serverError, ... }
├─ Internal module
│  └─ Fine-grained for actionable handling
│     enum PaymentError: Error {
│         case cardDeclined(reason: String)
│         case insufficientFunds(balance: Decimal, required: Decimal)
│     }
└─ User-facing
   └─ LocalizedError with user-friendly messages
      var errorDescription: String? { "Payment failed" }
The trap: Too many error types that no one handles differently. If every case has the same handler, collapse them.
错误类型应该有多具体?
├─ API 边界(框架、库)
│  └─ 粗粒度领域错误
│     enum NetworkError: Error { case timeout, serverError, ... }
├─ 内部模块
│  └─ 细粒度以支持可操作的处理
│     enum PaymentError: Error {
│         case cardDeclined(reason: String)
│         case insufficientFunds(balance: Decimal, required: Decimal)
│     }
└─ 面向用户
   └─ 带有友好提示信息的 LocalizedError
      var errorDescription: String? { "Payment failed" }
陷阱:定义过多无人区分处理的错误类型。如果所有错误分支的处理逻辑相同,应合并它们。

Recovery Strategy Selection

恢复策略选择

Is the error transient (network, timeout)?
├─ YES → Is it idempotent?
│  ├─ YES → Retry with exponential backoff
│  └─ NO → Retry cautiously or fail
└─ NO → Is recovery possible?
   ├─ YES → Can user fix it?
   │  ├─ YES → Show actionable error UI
   │  └─ NO → Auto-recover or fallback value
   └─ NO → Log and show generic error
错误是否是暂时性的(网络超时等)?
├─ 是 → 操作是否具有幂等性?
│  ├─ 是 → 使用指数退避策略重试
│  └─ 否 → 谨慎重试或直接失败
└─ 否 → 是否可恢复?
   ├─ 是 → 用户能否修复?
   │  ├─ 是 → 展示可操作的错误 UI
   │  └─ 否 → 自动恢复或使用回退值
   └─ 否 → 记录日志并展示通用错误

Error Presentation Selection

错误展示选择

How critical is the failure?
├─ Critical (can't continue)
│  └─ Full-screen error with retry
├─ Important (user needs to know)
│  └─ Alert dialog
├─ Minor (informational)
│  └─ Toast or inline message
└─ Silent (user doesn't need to know)
   └─ Log only, use fallback

失败的严重程度如何?
├─ 严重(无法继续操作)
│  └─ 全屏错误提示 + 重试按钮
├─ 重要(用户需要知晓)
│  └─ 弹窗提示
├─ 轻微(仅信息告知)
│  └─ 轻提示(Toast)或内联消息
└─ 静默(无需用户知晓)
   └─ 仅记录日志,使用回退方案

NEVER Do

绝对不要做的事

Error Type Design

错误类型设计

NEVER use generic catch-all errors:
swift
// ❌ Caller can't handle different cases
enum AppError: Error {
    case somethingWentWrong
    case error(String)  // Just a message — no actionable info
}

// ✅ Specific, actionable cases
enum AuthError: Error {
    case invalidCredentials
    case sessionExpired
    case accountLocked(unlockTime: Date?)
}
NEVER throw errors with sensitive information:
swift
// ❌ Leaks internal details
throw DatabaseError.queryFailed(sql: query, password: dbPassword)

// ✅ Sanitize sensitive data
throw DatabaseError.queryFailed(table: tableName)
NEVER create error enums with one case:
swift
// ❌ Pointless enum
enum ValidationError: Error {
    case invalid
}

// ✅ Use existing error or don't create enum
struct ValidationError: Error {
    let field: String
    let reason: String
}
绝对不要使用通用的万能错误类型:
swift
// ❌ 调用者无法区分处理不同情况
enum AppError: Error {
    case somethingWentWrong
    case error(String)  // 仅包含消息,无可用操作信息
}

// ✅ 具体、可操作的错误分支
enum AuthError: Error {
    case invalidCredentials
    case sessionExpired
    case accountLocked(unlockTime: Date?)
}
绝对不要抛出包含敏感信息的错误:
swift
// ❌ 泄露内部细节
throw DatabaseError.queryFailed(sql: query, password: dbPassword)

// ✅ 清理敏感数据
throw DatabaseError.queryFailed(table: tableName)
绝对不要创建仅含一个分支的错误枚举:
swift
// ❌ 无意义的枚举
enum ValidationError: Error {
    case invalid
}

// ✅ 使用现有错误类型或不创建枚举
struct ValidationError: Error {
    let field: String
    let reason: String
}

Error Propagation

错误传播

NEVER swallow errors silently:
swift
// ❌ Failure is invisible
func loadUser() async {
    try? await fetchUser()  // Error ignored, user is nil, no feedback
}

// ✅ Handle or propagate
func loadUser() async {
    do {
        user = try await fetchUser()
    } catch {
        errorMessage = error.localizedDescription
        analytics.trackError(error)
    }
}
NEVER catch and rethrow without adding context:
swift
// ❌ Pointless catch
do {
    try operation()
} catch {
    throw error  // Why catch at all?
}

// ✅ Add context or handle
do {
    try operation()
} catch {
    throw ContextualError.operationFailed(underlying: error, context: "during checkout")
}
NEVER use force-try (
try!
) outside of known-safe scenarios:
swift
// ❌ Crashes if JSON is malformed
let data = try! JSONEncoder().encode(untrustedInput)

// ✅ Safe scenarios only
let data = try! JSONEncoder().encode(staticKnownValue)  // Compile-time known
let url = URL(string: "https://example.com")!  // Literal string

// ✅ Handle unknown input
guard let data = try? JSONEncoder().encode(userInput) else {
    throw EncodingError.failed
}
绝对不要静默忽略错误:
swift
// ❌ 失败完全不可见
func loadUser() async {
    try? await fetchUser()  // 错误被忽略,user 为 nil,无任何反馈
}

// ✅ 处理或传播错误
func loadUser() async {
    do {
        user = try await fetchUser()
    } catch {
        errorMessage = error.localizedDescription
        analytics.trackError(error)
    }
}
绝对不要捕获错误后直接重新抛出而不添加上下文:
swift
// ❌ 无意义的捕获
do {
    try operation()
} catch {
    throw error  // 那为什么要捕获?
}

// ✅ 添加上下文或处理错误
do {
    try operation()
} catch {
    throw ContextualError.operationFailed(underlying: error, context: "during checkout")
}
绝对不要在非已知安全场景下使用强制 try (
try!
):
swift
// ❌ JSON 格式错误时会崩溃
let data = try! JSONEncoder().encode(untrustedInput)

// ✅ 仅在安全场景使用
let data = try! JSONEncoder().encode(staticKnownValue)  // 编译时已知安全
let url = URL(string: "https://example.com")!  // 字面量字符串

// ✅ 处理不可信输入
guard let data = try? JSONEncoder().encode(userInput) else {
    throw EncodingError.failed
}

CancellationError

CancellationError

NEVER show CancellationError to users:
swift
// ❌ User sees "cancelled" error when navigating away
func loadData() async {
    do {
        data = try await fetchData()
    } catch {
        errorMessage = error.localizedDescription  // Shows "cancelled"
    }
}

// ✅ Handle cancellation separately
func loadData() async {
    do {
        data = try await fetchData()
    } catch is CancellationError {
        return  // User navigated away — not an error
    } catch {
        errorMessage = error.localizedDescription
    }
}
绝对不要向用户展示 CancellationError:
swift
// ❌ 用户导航离开时会看到“已取消”错误
func loadData() async {
    do {
        data = try await fetchData()
    } catch {
        errorMessage = error.localizedDescription  // 显示“已取消”
    }
}

// ✅ 单独处理取消操作
func loadData() async {
    do {
        data = try await fetchData()
    } catch is CancellationError {
        return  // 用户已导航离开,不视为错误
    } catch {
        errorMessage = error.localizedDescription
    }
}

Retry Logic

重试逻辑

NEVER retry non-idempotent operations blindly:
swift
// ❌ May charge user multiple times
func processPayment() async throws {
    try await retryWithBackoff {
        try await chargeCard(amount)  // Not idempotent!
    }
}

// ✅ Use idempotency key or check state first
func processPayment() async throws {
    let idempotencyKey = UUID().uuidString
    try await retryWithBackoff {
        try await chargeCard(amount, idempotencyKey: idempotencyKey)
    }
}
NEVER retry without limits:
swift
// ❌ Infinite loop if server is down
func fetch() async throws -> Data {
    while true {
        do {
            return try await request()
        } catch {
            try await Task.sleep(nanoseconds: 1_000_000_000)
        }
    }
}

// ✅ Limited retries with backoff
func fetch(maxAttempts: Int = 3) async throws -> Data {
    var delay: UInt64 = 1_000_000_000
    for attempt in 1...maxAttempts {
        do {
            return try await request()
        } catch where attempt < maxAttempts && isRetryable(error) {
            try await Task.sleep(nanoseconds: delay)
            delay *= 2
        }
    }
    throw FetchError.maxRetriesExceeded
}

绝对不要盲目重试非幂等操作:
swift
// ❌ 可能导致用户被多次扣费
func processPayment() async throws {
    try await retryWithBackoff {
        try await chargeCard(amount)  // 不具备幂等性!
    }
}

// ✅ 使用幂等键或先检查状态
func processPayment() async throws {
    let idempotencyKey = UUID().uuidString
    try await retryWithBackoff {
        try await chargeCard(amount, idempotencyKey: idempotencyKey)
    }
}
绝对不要无限制重试:
swift
// ❌ 服务器宕机时会无限循环
func fetch() async throws -> Data {
    while true {
        do {
            return try await request()
        } catch {
            try await Task.sleep(nanoseconds: 1_000_000_000)
        }
    }
}

// ✅ 带退避策略的有限重试
func fetch(maxAttempts: Int = 3) async throws -> Data {
    var delay: UInt64 = 1_000_000_000
    for attempt in 1...maxAttempts {
        do {
            return try await request()
        } catch where attempt < maxAttempts && isRetryable(error) {
            try await Task.sleep(nanoseconds: delay)
            delay *= 2
        }
    }
    throw FetchError.maxRetriesExceeded
}

Essential Patterns

核心模式

Typed Error with Recovery Info

包含恢复信息的类型化错误

swift
enum NetworkError: Error, LocalizedError {
    case noConnection
    case timeout
    case serverError(statusCode: Int)
    case unauthorized

    var errorDescription: String? {
        switch self {
        case .noConnection: return "No internet connection"
        case .timeout: return "Request timed out"
        case .serverError(let code): return "Server error (\(code))"
        case .unauthorized: return "Session expired"
        }
    }

    var recoverySuggestion: String? {
        switch self {
        case .noConnection: return "Check your network settings"
        case .timeout: return "Try again"
        case .serverError: return "Please try again later"
        case .unauthorized: return "Please log in again"
        }
    }

    var isRetryable: Bool {
        switch self {
        case .noConnection, .timeout, .serverError: return true
        case .unauthorized: return false
        }
    }
}
swift
enum NetworkError: Error, LocalizedError {
    case noConnection
    case timeout
    case serverError(statusCode: Int)
    case unauthorized

    var errorDescription: String? {
        switch self {
        case .noConnection: return "无网络连接"
        case .timeout: return "请求超时"
        case .serverError(let code): return "服务器错误 (\(code))"
        case .unauthorized: return "会话已过期"
        }
    }

    var recoverySuggestion: String? {
        switch self {
        case .noConnection: return "检查你的网络设置"
        case .timeout: return "请重试"
        case .serverError: return "请稍后再试"
        case .unauthorized: return "请重新登录"
        }
    }

    var isRetryable: Bool {
        switch self {
        case .noConnection, .timeout, .serverError: return true
        case .unauthorized: return false
        }
    }
}

Result with Typed Error

搭配类型化错误的 Result

swift
func validate(email: String) -> Result<String, ValidationError> {
    guard !email.isEmpty else {
        return .failure(.emptyField("email"))
    }
    guard email.contains("@") else {
        return .failure(.invalidFormat("email"))
    }
    return .success(email)
}

// Forced exhaustive handling
switch validate(email: input) {
case .success(let email):
    createAccount(email: email)
case .failure(let error):
    showValidationError(error)
}
swift
func validate(email: String) -> Result<String, ValidationError> {
    guard !email.isEmpty else {
        return .failure(.emptyField("email"))
    }
    guard email.contains("@") else {
        return .failure(.invalidFormat("email"))
    }
    return .success(email)
}

// 强制穷举处理
switch validate(email: input) {
case .success(let email):
    createAccount(email: email)
case .failure(let error):
    showValidationError(error)
}

Retry with Exponential Backoff

指数退避重试

swift
func withRetry<T>(
    maxAttempts: Int = 3,
    initialDelay: Duration = .seconds(1),
    operation: () async throws -> T
) async throws -> T {
    var delay = initialDelay

    for attempt in 1...maxAttempts {
        do {
            return try await operation()
        } catch let error as NetworkError where error.isRetryable && attempt < maxAttempts {
            try await Task.sleep(for: delay)
            delay *= 2
        } catch {
            throw error
        }
    }
    fatalError("Should not reach")
}
swift
func withRetry<T>(
    maxAttempts: Int = 3,
    initialDelay: Duration = .seconds(1),
    operation: () async throws -> T
) async throws -> T {
    var delay = initialDelay

    for attempt in 1...maxAttempts {
        do {
            return try await operation()
        } catch let error as NetworkError where error.isRetryable && attempt < maxAttempts {
            try await Task.sleep(for: delay)
            delay *= 2
        } catch {
            throw error
        }
    }
    fatalError("Should not reach")
}

Error Presentation in ViewModel

ViewModel 中的错误展示

swift
@MainActor
final class ViewModel: ObservableObject {
    @Published private(set) var state: State = .idle

    enum State: Equatable {
        case idle
        case loading
        case loaded(Data)
        case error(String, isRetryable: Bool)
    }

    func load() async {
        state = .loading
        do {
            let data = try await fetchData()
            state = .loaded(data)
        } catch is CancellationError {
            state = .idle  // Don't show error
        } catch let error as NetworkError {
            state = .error(error.localizedDescription, isRetryable: error.isRetryable)
        } catch {
            state = .error("Something went wrong", isRetryable: true)
        }
    }
}

swift
@MainActor
final class ViewModel: ObservableObject {
    @Published private(set) var state: State = .idle

    enum State: Equatable {
        case idle
        case loading
        case loaded(Data)
        case error(String, isRetryable: Bool)
    }

    func load() async {
        state = .loading
        do {
            let data = try await fetchData()
            state = .loaded(data)
        } catch is CancellationError {
            state = .idle  // 不展示错误
        } catch let error as NetworkError {
            state = .error(error.localizedDescription, isRetryable: error.isRetryable)
        } catch {
            state = .error("出现未知问题", isRetryable: true)
        }
    }
}

Quick Reference

速查指南

Error Handling Decision Matrix

错误处理决策矩阵

ScenarioPatternWhy
Network call
throws
Exceptional failure
Validation
Result<T, E>
Expected failure, needs handling
Optional parsing
try?
Failure acceptable, use default
Must succeed
try!
Only for literals/static
Background taskLog onlyUser doesn't need to know
场景模式原因
网络请求
throws
失败属于异常情况
验证操作
Result<T, E>
失败可预期,需要显式处理
可选解析
try?
失败可接受,使用默认值
必须成功
try!
仅适用于字面量/静态已知安全场景
后台任务仅记录日志用户无需知晓

Error Type Checklist

错误类型检查清单

  • Cases are actionable (handler differs per case)
  • No sensitive data in associated values
  • Conforms to
    LocalizedError
    for user-facing
  • Has
    isRetryable
    property if recovery is possible
  • Has
    recoverySuggestion
    for user guidance
  • 错误分支具有可操作性(不同分支处理逻辑不同)
  • 关联值中无敏感数据
  • 面向用户的错误遵循
    LocalizedError
    协议
  • 若支持恢复则包含
    isRetryable
    属性
  • 包含
    recoverySuggestion
    为用户提供指导

Red Flags

危险信号

SmellProblemFix
catch { }
empty
Error silently swallowedLog or propagate
Same handler for all casesOver-specific error typeCollapse to fewer cases
try?
on critical path
Hidden failuresUse
do-catch
Retry without limitInfinite loop riskAdd max attempts
CancellationError shown to userBad UXHandle separately
error.localizedDescription
on NSError
Technical messageMap to user-friendly
问题症状危害修复方案
catch { }
空捕获块
错误被静默忽略记录日志或传播错误
所有错误分支使用相同处理逻辑错误类型过度细分合并为更少的分支
关键路径使用
try?
失败被隐藏使用
do-catch
处理
无限制重试存在无限循环风险添加最大重试次数
向用户展示 CancellationError糟糕的用户体验单独处理取消操作
对 NSError 使用
error.localizedDescription
展示技术化消息映射为用户友好的提示