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

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)                     // 7

Your 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 age

Pro Tips

Common Errors (and the fix)

📋 Quick Reference

TaskCodeNote
Error typeenum E: Error { case bad }conforms to Error
Throwing funcfunc f() throws -> Tmay fail
Raisethrow E.badinside f
Handledo { try f() } catch { … }full control
Optionaltry? f()value or nil
Forcetry! 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!

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

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.