Skip to content
AgileLogics
7 min read

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 into nil — 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