Error Handling

This section documents the Swift 6 language mode as shipped by Swift 6.3, as published in The Swift Programming Language at docs.swift.org, which is the reference these pages are written and verified against. 6.4-beta-only features are always flagged as such — never presented as baseline.

This content was generated with the assistance of AI and should be verified against docs.swift.org before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Swift models recoverable runtime failures as values conforming to the Error protocol, propagated explicitly through function signatures rather than thrown invisibly like an exception in other languages — a caller can always tell, from the signature alone, whether a call might fail.

The Error Protocol and Representing Errors

enum VendingMachineError: Error {
    case invalidSelection
    case insufficientFunds(coinsNeeded: Int)
    case outOfStock
}

struct ValidationError: Error {
    let field: String
    let reason: String
}

Error is an empty marker protocol — any type can conform, but an enum is by far the most common shape, since it naturally groups a closed set of related failure cases and lets each case carry its own associated data (like insufficientFunds’ coin count above). A `struct conformance suits an error family that instead wants shared stored properties across many similar failures. Nothing about conforming to Error requires throwing it; it is just an ordinary value until code decides to throw it.

throw and the Four Ways to Handle an Error

func vend(itemNamed name: String, coinsInserted: Int) throws -> String {
    guard name == "chips" else { throw VendingMachineError.invalidSelection }
    guard coinsInserted >= 2 else {
        throw VendingMachineError.insufficientFunds(coinsNeeded: 2 - coinsInserted)
    }
    return name
}

// 1. `throws` propagation: the caller re-throws too, so it also needs `throws` (or `try!`/`try?` below)
func buySnack(coins: Int) throws -> String {
    try vend(itemNamed: "chips", coinsInserted: coins)     // `try` is mandatory at every throwing call site
}

// 2. `do`/`catch` with pattern matching, including multi-pattern catch clauses
do {
    let item = try vend(itemNamed: "chips", coinsInserted: 1)
    print("Bought \(item)")
} catch VendingMachineError.invalidSelection, VendingMachineError.outOfStock {
    print("Can't buy that right now")                       // one clause handling two cases
} catch VendingMachineError.insufficientFunds(let coinsNeeded) {
    print("Insert \(coinsNeeded) more coins")
} catch {
    print("Unexpected error: \(error)")                       // catch-all: binds the implicit `error` constant
}

// 3. `try?` -- converts a thrown error to `nil`, discarding which error it was
let maybeItem = try? vend(itemNamed: "chips", coinsInserted: 5)     // String?

// 4. `try!` -- asserts the call cannot fail; traps at runtime if it does
let definitelyChips = try! vend(itemNamed: "chips", coinsInserted: 10)

throw immediately exits the current scope with an error value, exactly like return exits with a result — only a function, method, initializer, or closure explicitly marked throws (or rethrows, below) may contain a throw not inside its own do/catch. Every call to a throwing function must be marked with try (or try? or try!), which is what makes throwing call sites visible in the source rather than silent. do/catch pattern-matches the thrown error against each catch clause in order, exactly like a switch (see Pattern Matching) — a clause can list several patterns separated by commas to share one handler, bind associated values, or fall through to a final unconditional catch that binds the error to an implicit error constant of type any Error. try? and try! are shorthands for when the caller does not need do/catch’s full generality: `try? turns any thrown error into nil (wrapping a successful non-optional result in Optional if needed), useful when only success/failure matters and the specific error is not; try! asserts success is guaranteed and crashes the program if that assertion is wrong, so it belongs only where an invariant outside the type system already rules out failure.

Typed Throws (Swift 6)

enum ParseError: Error {
    case unexpectedToken(String)
    case unterminatedString
}

func parse(_ text: String) throws(ParseError) -> [String] {   // throws(ParseError): only ParseError can escape
    guard !text.isEmpty else { throw .unterminatedString }
    return text.split(separator: " ").map(String.init)
}

do {
    let tokens = try parse("a b c")
    print(tokens)
} catch {
    // `error` here is statically typed `ParseError`, not `any Error` -- exhaustive switches need no default
    switch error {
    case .unexpectedToken(let token): print("bad token: \(token)")
    case .unterminatedString: print("unterminated string")
    }
}

Plain throws is shorthand for throws(any Error): any conforming type may be thrown, and callers pay the cost of existential boxing for the error value. throws(MyError), introduced by SE-0413 (Swift 6), narrows the declared error type to one specific type (or Never, meaning the function cannot throw at all despite being written with throws), which lets a catch block bind error at that concrete static type — enabling exhaustive switch handling with no catch-all case — and avoids existential overhead in hot paths. Reach for it in performance-sensitive code or where a public API wants to document its exact failure surface in the type system itself; plain throws remains the right default when a function’s callers genuinely need to handle a heterogeneous mix of error types.

rethrows

func execute(_ times: Int, _ body: () throws -> Void) rethrows {
    for _ in 0..<times { try body() }
}

try execute(3) { print("ok") }                    // fine: the closure argument doesn't throw
try execute(3) { throw VendingMachineError.outOfStock }   // fine: execute only rethrows what body throws

rethrows marks a function that itself only throws when one of its own function-typed parameters throws — execute above has no unconditional throw of its own, only a try body() inside a loop. This lets a higher- order function stay non-throwing when called with a non-throwing closure (no try required at that call site) while still propagating a throwing closure’s errors when one is passed, which a plain throws signature cannot express without forcing every caller to write try regardless.

Result<Success, Failure> and Converting Between Throwing and Result

func fetchData(from url: String) -> Result<String, VendingMachineError> {
    guard url.hasPrefix("https") else { return .failure(.invalidSelection) }
    return .success("payload")
}

switch fetchData(from: "https://example.com") {
case .success(let payload): print("Got \(payload)")
case .failure(let error): print("Failed: \(error)")
}

// throwing -> Result
func asResult() -> Result<String, Error> {
    Result { try vend(itemNamed: "chips", coinsInserted: 2) }   // Result's throwing initializer captures the throw
}

// Result -> throwing
let value = try asResult().get()                    // `.get()` re-throws `.failure`'s error, returns `.success`'s value

Result<Success, Failure> is an ordinary two-case enum (.success(Success) / .failure(Failure)) that stores an outcome as a value rather than propagating it through the throws mechanism — useful when a result needs to be stored, passed around, or delivered later (e.g. into a completion handler) rather than handled immediately at the call site. Result’s `init(catching:) initializer bridges a throwing expression into a Result in one step, and .get() bridges back, re-throwing a stored .failure — the two directions Swift code most often needs when mixing throwing APIs with Result-based ones.

LocalizedError and CustomNSError

enum SignUpError: LocalizedError {
    case usernameTaken

    var errorDescription: String? {                 // shown to users, e.g. by SwiftUI's alert(error:)
        switch self {
        case .usernameTaken: "That username is already taken."
        }
    }
}

enum StorageError: CustomNSError {                    // bridges richly to NSError on Apple platforms
    case diskFull

    static var errorDomain: String { "com.example.Storage" }
    var errorCode: Int { 1 }
    var errorUserInfo: [String: Any] {
        [NSLocalizedDescriptionKey: "The disk is full."]
    }
}

LocalizedError refines Error with optional user-facing text (errorDescription, failureReason, recoverySuggestion, helpAnchor) that platform UI can display directly instead of a raw enum case name. CustomNSError instead controls how a Swift error bridges to Objective-C’s NSError — its domain, code, and userInfo dictionary — relevant wherever an API still surfaces errors through NSError (see Interoperability with C, Objective-C, and C++).

defer for Cleanup

func processFile(named filename: String) throws {
    let file = FileHandle(forReadingAtPath: filename)
    defer { file?.closeFile() }                     // runs on every exit path: return, throw, or falling off the end

    guard let file else { throw ValidationError(field: "filename", reason: "not found") }
    // ... use file ...
}                                                     // closeFile() runs here even when the guard above throws

A defer block runs when execution leaves the current scope, regardless of how it leaves — a normal return, a throw, or simply reaching the end — which makes it the idiomatic place for cleanup (closing a file, releasing a lock, undoing a partial mutation) that must happen exactly once no matter which exit path is taken. Multiple defer blocks in the same scope run in reverse order of their appearance, mirroring how nested resource acquisition is typically unwound.

Errors in async Code

func fetchUser(id: String) async throws -> String {
    try await Task.sleep(for: .seconds(1))
    guard !id.isEmpty else { throw ValidationError(field: "id", reason: "empty") }
    return "user-\(id)"
}

Task {
    do {
        let user = try await fetchUser(id: "42")
        print(user)
    } catch {
        print("fetch failed: \(error)")
    }
}

async and throws compose freely on the same declaration (written async throws, always in that order) and behave exactly as each does alone: try await marks a call site that can both suspend and fail, and do/catch around it works identically to synchronous code. Error propagation crossing an await boundary raises no extra concerns beyond the usual async/await rules covered in Async/Await and Tasks.

Errors vs. fatalError, precondition, and assert

func withdraw(_ amount: Int, from balance: inout Int) throws {
    guard amount <= balance else { throw ValidationError(field: "amount", reason: "exceeds balance") }
    balance -= amount
}

func configure(mode: Int) {
    precondition(mode >= 0, "mode must be non-negative")   // checked in both debug and release
    assert(mode < 100, "mode should realistically stay under 100")   // checked only in debug builds
    guard mode != -1 else { fatalError("mode -1 is reserved and must never reach configure(_:)") }
}

A thrown Error models a recoverable condition the caller is expected to handle — invalid user input, a network failure, a missing file. precondition, assert, and fatalError instead model programmer errors — violations of an invariant that should never happen if the code calling in is correct — and respond by trapping the process rather than returning control to a catch block: precondition and fatalError always check (and crash) in both debug and release builds, while assert is stripped from release builds entirely and exists purely as a debug-time sanity net. Choosing between the two families is really a question of who is expected to react to the failure: an error a caller can meaningfully recover from belongs in throws; a condition that only a bug in the calling code itself could produce belongs in precondition/assert/fatalError.

Choosing Between Optional, Result, throws, and a Trap

flowchart TD A[A call can fail -- how should failure be represented?] --> B{Is failure a caller bug,\nnot a runtime condition?} B -- yes --> C[fatalError / precondition / assert] B -- no --> D{Does the caller need to know *why* it failed?} D -- no, success/failure is enough --> E{Is the outcome needed immediately\nor stored/delivered later?} E -- immediately --> F[Optional] E -- later, e.g. a completion handler --> G[Result] D -- yes, the specific error matters --> H{Should the compiler enforce\nhandling at the call site?} H -- yes --> I[throws / throws Type] H -- no, store or pass around --> G

The four mechanisms above answer different questions rather than competing on the same axis: Optional says only "did this produce a value or not," discarding any reason; Result carries a specific failure value like throws does, but as an ordinary stored value rather than something the language forces the caller to try; throws (or throws(SpecificError), Swift 6) makes the possibility of failure part of the function’s signature, which the compiler enforces at every call site; and a trap (fatalError/precondition/assert) says the condition should be impossible given a correct caller, so it terminates rather than returning any value at all.

See Also