error-handling-patterns

Error Handling Patterns — Expert Decisions

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "error-handling-patterns" with this command: npx skills add kaakati/rails-enterprise-dev/kaakati-rails-enterprise-dev-error-handling-patterns

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.

Decision Trees

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.

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.

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

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

NEVER Do

Error Type Design

NEVER use generic catch-all errors:

// ❌ 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:

// ❌ 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:

// ❌ Pointless enum enum ValidationError: Error { case invalid }

// ✅ Use existing error or don't create enum struct ValidationError: Error { let field: String let reason: String }

Error Propagation

NEVER swallow errors silently:

// ❌ 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:

// ❌ 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:

// ❌ 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 }

CancellationError

NEVER show CancellationError to users:

// ❌ 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 } }

Retry Logic

NEVER retry non-idempotent operations blindly:

// ❌ 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:

// ❌ 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 }

Essential Patterns

Typed Error with Recovery Info

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

}

Result with Typed Error

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

Retry with Exponential Backoff

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 &#x26;&#x26; attempt &#x3C; maxAttempts {
        try await Task.sleep(for: delay)
        delay *= 2
    } catch {
        throw error
    }
}
fatalError("Should not reach")

}

Error Presentation in ViewModel

@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)
    }
}

}

Quick Reference

Error Handling Decision Matrix

Scenario Pattern Why

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 task Log only User doesn't need to know

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

Red Flags

Smell Problem Fix

catch { } empty Error silently swallowed Log or propagate

Same handler for all cases Over-specific error type Collapse to fewer cases

try? on critical path Hidden failures Use do-catch

Retry without limit Infinite loop risk Add max attempts

CancellationError shown to user Bad UX Handle separately

error.localizedDescription on NSError Technical message Map to user-friendly

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

flutter conventions & best practices

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

getx state management patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

ruby oop patterns

No summary provided by upstream source.

Repository SourceNeeds Review