Swift Error Handling
Programs encounter problems — a file might not exist, a network call might fail, or a user might enter invalid data. Swift's error handling system gives you a structured way to detect these problems, communicate them clearly, and recover gracefully.
The Vending Machine Analogy
┌──────────────────────────────────────────────────┐
│ Buying a snack from a vending machine │
│ │
│ Possible failures: │
│ ❌ Item out of stock │
│ ❌ Not enough coins inserted │
│ ❌ Machine out of change │
│ │
│ Each failure has a name and a cause. │
│ The machine reports WHY it failed — │
│ not just that something went wrong. │
└──────────────────────────────────────────────────┘
Step 1 – Define Error Types
enum VendingError: Error {
case outOfStock
case insufficientFunds(needed: Double)
case invalidSelection
}
Errors in Swift conform to the Error protocol. Using an enum gives every error a specific, descriptive name. Associated values let you attach extra information, like the amount still needed.
Step 2 – Throwing Functions
func buySnack(item: String, paid: Double) throws -> String {
let price = 25.0
guard item == "Chips" else {
throw VendingError.invalidSelection
}
guard paid >= price else {
throw VendingError.insufficientFunds(needed: price - paid)
}
return "Here is your \(item)!"
}
The throws keyword in the function signature signals that this function might fail. The throw keyword sends an error to the caller instead of returning a normal value.
Step 3 – Catching Errors with do-try-catch
do {
let result = try buySnack(item: "Chips", paid: 10.0)
print(result)
} catch VendingError.insufficientFunds(let needed) {
print("Insert ₹\(needed) more.")
} catch VendingError.invalidSelection {
print("Item not available.")
} catch VendingError.outOfStock {
print("Item is sold out.")
} catch {
print("Unexpected error: \(error)")
}
// Insert ₹15.0 more.
Error Flow Diagram
┌──────────────────────────────────────────────────────┐
│ try buySnack(item: "Chips", paid: 10.0) │
│ │ │
│ paid < price? │
│ │ │
│ YES → throw insufficientFunds(15.0) │
│ │ │
│ catch VendingError.insufficientFunds(let n) │
│ │ │
│ print("Insert ₹15.0 more.") │
└──────────────────────────────────────────────────────┘
try? – Convert Error to Optional
let result = try? buySnack(item: "Soda", paid: 30.0)
print(result ?? "Purchase failed") // Purchase failed
try? calls a throwing function and returns nil if it throws. Use it when you do not need to know which error occurred — just whether it succeeded.
try! – Force Try (Avoid in Production)
let result = try! buySnack(item: "Chips", paid: 50.0)
print(result) // Here is your Chips!
try! crashes the app if an error is thrown. Only use it when you are absolutely certain the function will not fail — typically in tests or playground experiments.
Rethrowing Functions
func performAction(_ action: () throws -> Void) rethrows {
try action()
}
try performAction {
throw VendingError.outOfStock
}
A rethrows function only throws when the closure it receives also throws. If the closure is non-throwing, the function behaves as non-throwing too.
defer – Always Run Cleanup Code
func readFile(name: String) throws {
print("Opening file")
defer {
print("Closing file") // runs no matter what
}
guard name == "data.txt" else {
throw VendingError.invalidSelection
}
print("Reading file contents")
}
try? readFile(name: "other.txt")
// Opening file
// Closing file
defer schedules code to run when the current scope exits — whether it exits normally or through an error. It is perfect for cleanup tasks like closing files or releasing resources.
