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
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
-
Optionals —
Optionalas the lightest-weight "did this work" signal, andguard/if letunwrapping. -
Functions —
throws/rethrows/asyncin a function’s full signature grammar. -
Pattern Matching — the pattern syntax
catchclauses share withswitch. -
Async/Await and Tasks —
async throwsand structured concurrency’s own error propagation across child tasks. -
Interoperability with C, Objective-C, and C++ — bridging
ErrortoNSErrorin full.