Error Handling (do/try/catch)
Reviewed & published by Brayan K
Real programs fail — networks drop, input is bad, files go missing. Swift handles this cleanly with the Error protocol, throws functions, and do/try/catch. You'll also meet try? and try!.
Part of the free Swift course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
- • Define error types that conform to Error
- • Mark functions throws and raise errors with throw
- • Handle failures with do / try / catch
- • Match specific errors in separate catch clauses
- • Use try? to get an optional result
- • Know when (rarely) try! is acceptable
1️⃣ Errors, throws & do/try/catch
Define an error type (usually an enum conforming to Error), mark a failing function throws, raise errors with throw, and call it inside a do block with try.
// 1) Define an error type — usually an enum conforming to Error.
enum LoginError: Error {
case emptyUsername
case passwordTooShort
}
// 2) A function that can fail is marked 'throws'. Raise errors with 'throw'.
func login(user: String, password: String) throws -> String {
if user.isEmpty { throw LoginError.emptyUsername }
if password.count < 6 { throw LoginError.passwordTooShort }
return "Welcome, \(user)!"
}
// 3) Call it inside do/try/catch.
do {
let message = try login(user: "ada", password: "secret123")
print(message) // Welcome, ada!
} catch LoginError.emptyUsername {
print("Username can't be empty")
} catch LoginError.passwordTooShort {
print("Password is too short")
} catch {
print("Unknown error: \(error)")
}2️⃣ Matching Specific Errors
Multiple catch clauses let you respond differently to each kind of failure, from most specific to a final general catch.
enum LoginError: Error { case emptyUsername, passwordTooShort }
func login(user: String, password: String) throws -> String {
if user.isEmpty { throw LoginError.emptyUsername }
if password.count < 6 { throw LoginError.passwordTooShort }
return "Welcome, \(user)!"
}
// This time the password is too short, so the matching catch runs.
do {
let message = try login(user: "ada", password: "123")
print(message)
} catch LoginError.emptyUsername {
print("Username can't be empty")
} catch LoginError.passwordTooShort {
print("Password is too short") // ← this one runs
} catch {
print("Unknown error: \(error)")
}3️⃣ try? and try!
Sometimes you don't need a full do/catch. try? turns a throwing call into an optional (value or nil). try! assumes success and crashes on error — reserve it for cases where failure is impossible.
enum ParseError: Error { case notANumber }
func parse(_ text: String) throws -> Int {
guard let n = Int(text) else { throw ParseError.notANumber }
return n
}
// try? → returns an OPTIONAL: the value, or nil if it threw. No do/catch.
let a = try? parse("42") // Optional(42)
let b = try? parse("nope") // nil
print(a ?? -1) // 42
print(b ?? -1) // -1
// try! → assumes success; CRASHES if it throws. Only when you're certain.
let c = try! parse("7") // 7 (safe: "7" is clearly a number)
print(c) // 7Your turn. Fill in the blanks to throw and then call safely with try?.
// 🎯 YOUR TURN — fill in the blanks, then run it in SwiftFiddle.
enum AgeError: Error { case negative }
// 1) Mark the function as throwing. Replace ___ with the keyword.
func checkAge(_ age: Int) ___ -> String { // 👉 replace ___ with 'throws'
if age < 0 { throw AgeError.negative } // raise the error
return "Age is \(age)"
}
// 2) Call it safely as an optional. Replace ___ with the right keyword.
let result = ___ checkAge(-5) // 👉 replace ___ with 'try?'
print(result ?? "Invalid age") // Invalid agePro Tips
- 💡 Use enums for errors. Each case is a clear, matchable failure.
- 💡 Order catches specific → general. End with a catch-all catch.
- 💡 Prefer try? when you only care whether it worked.
- 💡 Avoid try! on real input. Reserve it for values you fully control.
Common Errors (and the fix)
- "Call can throw but is not marked with 'try'" — add try before the throwing call.
- "Errors thrown from here are not handled" — wrap the call in do/catch, or use try?/try!.
- "Fatal error" from try! — the call threw; switch to try? or full do/catch.
- Unreachable catch — put specific catches before the general catch.
📋 Quick Reference
| Task | Code | Note |
|---|---|---|
| Error type | enum E: Error { case bad } | conforms to Error |
| Throwing func | func f() throws -> T | may fail |
| Raise | throw E.bad | inside f |
| Handle | do { try f() } catch { … } | full control |
| Optional | try? f() | value or nil |
| Force | try! f() | crash on error |
Mini-Challenge: Bank Withdrawal
No blanks this time — just a brief and an outline. Build it, run it, and check your output against the example in the comments.
// 🎯 MINI-CHALLENGE: a tiny bank withdrawal
// 1. Define an enum BankError: Error with cases .insufficientFunds and
// .invalidAmount.
// 2. Write withdraw(amount: Int, balance: Int) throws -> Int that:
// - throws .invalidAmount if amount <= 0
// - throws .insufficientFunds if amount > balance
// - otherwise returns the new balance (balance - amount)
// 3. Call it in a do/try/catch with amount 30, balance 100, and print the
// new balance. Then try amount 200 and print a message in the catch.
//
// ✅ Expected output:
// New balance: 70
// Not enough money
// your code here🎉 Lesson Complete!
- ✅ Error types conform to the Error protocol (often an enum)
- ✅ throws marks a failing function; throw raises an error
- ✅ do/try/catch handles failures with full control
- ✅ try? gives an optional; try! crashes on error
- ✅ Multiple catch clauses match specific errors
Practice quiz
What protocol must a Swift error type adopt?
- Codable
- Throwable
- Error
- Failure
Answer: Error. Custom error types conform to the Error protocol — commonly an enum that adopts Error.
Which keyword marks a function that can throw an error?
- throws
- throwing
- error
- risky
Answer: throws. A function that can throw is declared with 'throws' before the return arrow: func f() throws -> T.
How do you actually raise an error inside a function?
- raise error
- return error
- error()
- throw someError
Answer: throw someError. You raise an error with the 'throw' keyword: throw MyError.badInput.
Which block structure handles a throwing call?
- if/else
- do/try/catch
- guard/let
- switch/case
Answer: do/try/catch. You call throwing code inside a 'do' block with 'try', and handle failures in 'catch'.
What keyword precedes a throwing call inside a do block?
- try
- catch
- throws
- do
Answer: try. Each call that can throw must be marked with 'try'.
What does 'try?' do?
- Crashes on error
- Ignores the function
- Converts the result to an optional — nil on error
- Re-throws the error
Answer: Converts the result to an optional — nil on error. try? turns a throwing call into an optional: you get the value, or nil if it threw.
What does 'try!' do?
- Returns an optional
- Force-unwraps — crashes if the call throws
- Catches the error silently
- Retries the call
Answer: Force-unwraps — crashes if the call throws. try! assumes success; if the call throws, the program crashes — use only when failure is impossible.
After a successful 'try?', what is the result's type for a function returning Int?
- Int
- Error
- Bool
- Int?
Answer: Int?. try? wraps the return value in an optional, so a throwing -> Int becomes Int? at the call site.
Can a catch block bind the specific error that was thrown?
- No
- Yes — catch let error, or pattern-match specific cases
- Only with try!
- Only in protocols
Answer: Yes — catch let error, or pattern-match specific cases. catch can pattern-match specific cases or bind the error, e.g. catch MyError.badInput or catch let e.
Why does Swift require 'try' on every throwing call?
- For performance
- To allow nil
- To make potential failure points visible in the code
- It's optional styling
Answer: To make potential failure points visible in the code. Marking each throwing call with try makes the points where errors can occur explicit and visible.
Continue this course
- Previous: Protocol-Oriented Programming
- Next: guard & defer — Exit early with guard and schedule cleanup with defer
Frequently asked questions
How do I define a custom error in Swift?
Create a type that conforms to the Error protocol — most commonly an enum, where each case is a distinct failure: enum LoginError: Error { case emptyUsername, passwordTooShort }. You can also use structs or classes, and you can attach associated values to carry details about the failure.
What is the difference between throws, try, and throw?
throws marks a function declaration as capable of failing (func f() throws). throw is the statement that actually raises an error inside such a function (throw MyError.bad). try is written at the call site before each throwing call to make the potential failure visible. All three work together.
What is the difference between try, try?, and try!?
Plain 'try' must be used inside a do/catch block and lets you handle the error. 'try?' converts the result into an optional — you get the value or nil if it threw, with no do/catch needed. 'try!' force-unwraps and crashes if the call throws, so only use it when you are certain it cannot fail.
How do I handle specific errors in a catch block?
A do block can have multiple catch clauses. You can pattern-match specific cases (catch LoginError.emptyUsername), bind associated values, or catch everything with a final general 'catch' that binds the error to the implicit constant 'error'. Order them from most specific to most general.
Why does Swift make me write try on every throwing call?
It makes the points where your code can fail explicit and easy to spot when reading. Unlike exceptions that can appear from anywhere, Swift's marked try keeps error propagation visible and the control flow predictable, which makes code easier to reason about and review.