Error Handling
Prerequisites: Enums with associated values
Some operations can fail in ways the caller should handle — a file is missing, a network request times out, an amount is invalid. Swift models these with thrown errors.
Defining errors
Any type that conforms to the Error protocol can be thrown. Enums are
the usual choice:
enum WithdrawalError: Error {
case invalidAmount
case insufficientFunds(needed: Double)
}
Throwing
A function that can fail is marked throws, and uses throw to
report a problem:
func withdraw(_ amount: Double, from balance: Double) throws -> Double {
guard amount > 0 else {
throw WithdrawalError.invalidAmount
}
guard amount <= balance else {
throw WithdrawalError.insufficientFunds(needed: amount - balance)
}
return balance - amount
}
Catching with do / try / catch
Every call to a throwing function must be marked with try, so you can
always see where errors may come from:
do {
let left = try withdraw(30, from: 100)
print("Left: \(left)")
_ = try withdraw(200, from: left)
print("This line is skipped")
} catch WithdrawalError.insufficientFunds(let needed) {
print("Short by \(needed)")
} catch {
print("Other error: \(error)")
}
Left: 70.0
Short by 130.0
When an error is thrown, the rest of the do block is skipped and the
first matching catch runs. A plain catch handles anything, with the
error available as error.
try? and try!
let result = try? withdraw(500, from: 100)
print(result == nil)
let amount = (try? withdraw(10, from: 100)) ?? 0
print(amount)
true
90.0
try?turns an error intonil— handy when you don't care why it failed.try!crashes if an error is thrown. Only use it when failure would be a bug in your own code.
Passing errors up
A throwing function can call other throwing functions with try and
let their errors pass straight through to its own caller — no
do/catch needed until the level that can actually handle them.
Typed throws (Swift 6)
Swift 6 lets a function declare exactly which error type it throws:
func withdraw(...) throws(WithdrawalError) -> Double. Callers then
know error is a WithdrawalError in catch. Plain throws remains
the right default for most code.
Common mistakes
- Calling a throwing function without
try:call can throw but is not marked with 'try'. - Using
try?everywhere and silently losing useful error information. - Using errors for things that aren't failures. If "not found" is a normal outcome, return an optional instead.
Practice
Write func parseAge(_ text: String) throws -> Int that throws
AgeError.notANumber for non-numeric text and AgeError.outOfRange for
values outside 0–150. Call it with "200" inside do/catch and print
the error.
Show solution
enum AgeError: Error {
case notANumber
case outOfRange
}
func parseAge(_ text: String) throws -> Int {
guard let age = Int(text) else { throw AgeError.notANumber }
guard (0...150).contains(age) else { throw AgeError.outOfRange }
return age
}
do {
print(try parseAge("200"))
} catch {
print("Error: \(error)")
}
Error: outOfRange