Things go wrong in every real app. A file can't be found, the network is offline, a user types an invalid date, a payment is declined. Swift handles these situations with error handling: a function that can fail is marked throws, it throws an error describing the problem, and the code that calls it must deal with that possibility explicitly.
Swift's approach is designed to be clear and safe. Every call that might fail is marked with try, so you can see at a glance where errors can happen. Errors are usually enums, which makes the possible failures easy to list and to handle case by case. And unlike some languages, Swift errors aren't hidden exceptions that can appear anywhere: the compiler makes sure you either handle them or pass them on.
In this lesson you'll define your own errors, write throwing functions, catch errors with do-catch, convert failures into optionals with try?, clean up with defer, and use the Result type.
Defining and throwing errors
Any type that adopts the Error protocol can be thrown. Enums are the natural choice:
enum BankError: Error {
case insufficientFunds(needed: Double)
case accountLocked
case invalidAmount
}
func withdraw(_ amount: Double, from balance: Double) throws -> Double {
guard amount > 0 else { throw BankError.invalidAmount }
guard amount <= balance else {
throw BankError.insufficientFunds(needed: amount - balance)
}
return balance - amount
}throwsin the declaration says the function can fail.throwstops the function immediately and sends the error to the caller.- Associated values carry details, such as how much more money was needed.
do, try and catch
Call a throwing function with try, inside a do block, and handle errors in catch blocks:
do {
let left = try withdraw(150, from: 100)
print("Left: \(left)")
} catch BankError.insufficientFunds(let needed) {
print("You need £\(needed) more")
} catch {
print("Something went wrong: \(error)")
}- Catch blocks are checked in order; a specific pattern can match an error case and extract its values.
- A final
catchwith no pattern catches everything, with the error available aserror. - A function that calls throwing code without catching it can mark itself
throwsand pass the error up to its caller.
try? and try!
try?turns the result into an optional: the value on success,nilon failure. It's ideal when you don't care why something failed:let data = try? loadFile().try!asserts that the call can't fail, and crashes if it does. Use it only for things that genuinely can't fail, like loading a resource bundled with your app.
For messages shown to users, adopt LocalizedError and provide errorDescription, so error.localizedDescription gives friendly text instead of a technical name.
defer and Result
defer { ... } schedules code to run when the current scope exits, however it exits: normally, by return, or by a thrown error. It's ideal for clean-up such as closing a file or stopping a loading spinner.
Result<Success, Failure> is an enum with .success(value) and .failure(error) cases. It's useful when you need to store or pass around an outcome, for example in a completion handler: completion(.success(user)). Calling try result.get() turns it back into a normal throwing call.
Swift Errors Syntax
Error handling
enum MyError: Error { case notFound, invalid(reason: String) }
func load(_ id: Int) throws -> String {
guard id > 0 else { throw MyError.invalid(reason: "id must be positive") }
return "item"
}
do {
let x = try load(1)
} catch MyError.invalid(let reason) {
print(reason)
} catch {
print(error)
}
let maybe = try? load(-1) // nil
defer { cleanUp() }
let r: Result<Int, MyError> = .success(1)Swift Errors Summary
- Errors adopt the Error protocol; enums with associated values describe failures well.
- Mark failing functions throws; throw sends an error to the caller.
- Call them with try inside do, and handle errors in catch blocks, specific ones first.
- try? converts failure to nil; try! crashes on failure and should be rare.
- defer runs clean-up when a scope exits; Result stores a success or a failure.
Swift Errors Examples
Each example shows the code, the output it produces and what each part does.
A throwing withdrawal
enum BankError: Error {
case invalidAmount
case insufficientFunds(needed: Double)
}
func withdraw(_ amount: Double, from balance: Double) throws -> Double {
guard amount > 0 else { throw BankError.invalidAmount }
guard amount <= balance else {
throw BankError.insufficientFunds(needed: amount - balance)
}
return balance - amount
}
for amount in [30.0, 150.0, -5.0] {
do {
let left = try withdraw(amount, from: 100)
print("Withdrew \(amount), £\(left) left")
} catch BankError.insufficientFunds(let needed) {
print("Can't withdraw \(amount): need £\(needed) more")
} catch {
print("Can't withdraw \(amount): \(error)")
}
}Withdrew 30.0, £70.0 left
Can't withdraw 150.0: need £50.0 more
Can't withdraw -5.0: invalidAmountLine-by-line explanation
- Line 6throws marks the function as able to fail.
- Line 9The error carries how much more money was needed.
- Line 16try marks the call that might throw.
- Lines 18 to 19A specific catch matches one error case and extracts its value.
- Lines 20 to 21A general catch handles everything else; printing an enum error shows its case name.
try? and friendly messages
import Foundation
enum ValidationError: LocalizedError {
case empty
case tooShort(minimum: Int)
var errorDescription: String? {
switch self {
case .empty: return "Please enter a password."
case .tooShort(let min): return "Use at least \(min) characters."
}
}
}
func validate(password: String) throws -> String {
if password.isEmpty { throw ValidationError.empty }
if password.count < 8 { throw ValidationError.tooShort(minimum: 8) }
return password
}
for attempt in ["", "abc", "correcthorse"] {
do {
_ = try validate(password: attempt)
print("Accepted")
} catch {
print(error.localizedDescription)
}
}
print((try? validate(password: "abc")) == nil)Please enter a password.
Use at least 8 characters.
Accepted
trueLine-by-line explanation
- Line 3LocalizedError lets each error provide a message for users.
- Lines 7 to 12errorDescription returns friendly text for each case.
- Line 26localizedDescription uses that text.
- Line 29try? turns the failure into nil.
defer and Result
enum LoadError: Error { case offline }
func loadProfile(online: Bool) -> Result<String, LoadError> {
print("Spinner on")
defer { print("Spinner off") }
guard online else { return .failure(.offline) }
return .success("Ada Lovelace")
}
for online in [true, false] {
switch loadProfile(online: online) {
case .success(let name): print("Loaded \(name)")
case .failure(let error): print("Failed: \(error)")
}
}Spinner on
Spinner off
Loaded Ada Lovelace
Spinner on
Spinner off
Failed: offlineLine-by-line explanation
- Line 5defer runs when the function exits, on both the success and the failure paths.
- Lines 6 to 7Result returns either a success with a value or a failure with an error, without throwing.
- Lines 11 to 14A switch handles both cases of the Result enum.
Common Swift Errors Mistakes
Calling a throwing function without try
let left = withdraw(50, from: 100) // error: call can throw but is not marked with 'try'let left = try withdraw(50, from: 100) // inside do-catch, or a throwing functiontry! on user input
let date = try! parseDate(userInput) // crashes on any invalid inputif let date = try? parseDate(userInput) {
schedule(on: date)
} else {
showError("Please enter a valid date.")
}Catching and ignoring every error
do { try save(order) } catch { } // the order silently isn't saveddo {
try save(order)
} catch {
showError("Couldn't save your order. Please try again.")
log(error)
}Swift Errors in Real Projects
Errors in iOS apps
Apple's APIs throw errors for file access, JSON decoding and networking, so error handling appears in almost every real app. Good apps turn those errors into helpful messages, offer to retry, and log the details for developers. Modern async code combines throws with async, so network calls read as let user = try await api.loadUser(), as the concurrency lesson shows.
Swift Errors FAQ
What is the difference between try, try? and try! in Swift?
try calls a throwing function and lets the error be caught by a catch or passed up to the caller. try? converts any error into nil, so you get an optional result. try! assumes no error will happen and crashes the app if one does, so keep it for failures that would be programming mistakes.
Do I have to catch every error in Swift?
No. A function that calls throwing code can be marked throws itself, and the error is passed to its caller. Somewhere up the chain the error must be handled with do/catch or converted with try?, and the compiler checks this for you.
How do I show a user-friendly error message in Swift?
Make your error type adopt LocalizedError and implement errorDescription to return the message for each case. error.localizedDescription then returns that text, which you can show in an alert or label instead of a technical type name.
When does a defer block run in Swift?
Just before the current scope ends, whether it ends normally, through return, or because an error was thrown. If a scope has several defer blocks, they run in reverse order, last one first.
Why does my catch block say "error" when I never declared it?
A catch without a pattern automatically provides a constant called error holding the thrown value. Its type is any Error, so use if let with as? or a specific catch pattern when you need the details of a particular error type.
Swift Errors Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Complete the call to a throwing function.
do {
let data = loadFile("notes.txt")
} catch {
print(error)
}
Which protocol must a type adopt to be thrown?
What does this print?
enum AgeError: Error { case negative }
func check(_ age: Int) throws -> Int {
if age < 0 { throw AgeError.negative }
return age
}
let a = try? check(-3)
print(a ?? 0)
When does a defer block run?
When is try! acceptable?
Swift Errors Quiz
5 questions to check what you learned.
What does
throwsin a function declaration mean?- The function can throw an error that callers must handle
- The function always fails
- The function returns nil
- The function is asynchronous
How do you handle one specific error case and extract its value?
- catch error
- catch MyError.someCase(let value)
- if error == someCase
- try?
What does LocalizedError's errorDescription provide?
- The error's line number
- A stack trace
- A user-friendly message, used by localizedDescription
- An error code
What are the two cases of Result?
- success and failure
- some and none
- ok and error
- true and false
What can a function do instead of catching an error from code it calls?
- Nothing, it must catch
- Use try! always
- Return nil automatically
- Mark itself throws and let the error propagate to its caller