Throwing and catching errorsswift-6.4/ios-26
Lesson 1 / 6
Unit 5 · Error handling

Throwing and catching errors

Coming from
Note

Unit 5 · Error handling. Optionals (Unit 3) model "no value"; errors model "it failed, and here's why." This unit is how Swift carries that why up to whoever can act on it.

Some operations don't just fail — they fail for a reason the caller might need. An optional can only say "nothing." Swift's error handling adds a separate channel that carries a typed reason from where a failure happens to where it's handled, without polluting the normal return value. It's built from three keywords: throws, throw, and try.

Defining an error and throwing it

Any type conforming to the empty Error protocol can be thrown. An enum is the natural fit — one case per failure mode:

enum FileError: Error {
    case notFound
    case tooLarge(bytes: Int)
    case permissionDenied
}

A function that can fail is marked throws, and it raises a failure with throw:

func load(_ name: String) throws -> Data {
    guard exists(name) else { throw FileError.notFound }
    let size = sizeOf(name)
    guard size < 10_000_000 else { throw FileError.tooLarge(bytes: size) }
    return read(name)
}

The associated value (bytes:) lets an error carry context, not just a label — the handler can report exactly how large the file was.

Catching with do / catch

Call a throwing function with try, inside a do block, and handle failures in catch:

do {
    let data = try load("report.pdf")
    show(data)
} catch FileError.notFound {
    show("No such file")
} catch FileError.tooLarge(let bytes) {
    show("Too big: \(bytes) bytes")
} catch {
    show("Unexpected: \(error)")   // `error` is bound implicitly
}

catch clauses pattern-match errors exactly like a switch, including binding associated values. The final bare catch binds the error to an implicit constant named error and handles anything not matched above.

try, try?, and try!

There are three ways to spell the try:

let data = try load(name)      // propagates the error to the caller (needs throws / do-catch)
let maybe = try? load(name)    // Data? — nil on any error, discarding the reason
let forced = try! load(name)   // Data — crashes if it throws (a claim it can't fail)
  • try — the default. The error flows up to your do/catch or out of a throws function.
  • try? — "I don't care why it failed, just give me nil." Converts the error channel into an optional. Use it when the reason genuinely doesn't matter.
  • try! — "this cannot fail here." Like force-unwrap, it traps if you're wrong. Same discipline: only when you can defend it.
Note

try? throws the reason away. It collapses every distinct error into a single nil, so you lose the diagnostic the error type was carrying. That's fine for "does this string parse at all?" and wrong for anything where the user needs to know why it failed. Reach for full do/catch when the reason matters.

Coming from Kotlin

Kotlin has no checked exceptions — nothing forces you to acknowledge that a call can fail. Swift's throws is checked: the compiler makes you try (and either handle or re-propagate), so a failure path can't be silently ignored. try? is the closest thing to Kotlin's "just wrap it in a nullable and move on."

Your turn

Define an error enum and throw the right case from a parser.

Parse a positive integer with throws
Edit the code on the right, then run the hidden tests.

Knowledge check

Q: What must a type do to be throwable, and why is an enum a common choice? Conform to the Error protocol (which has no requirements). An enum is common because one case per failure mode gives callers something precise to catch and pattern-match.

Q: When is try? the wrong tool? When the caller needs to know why the call failed. try? discards the error and yields nil, so any distinction between failure modes is lost. Use do/catch there.

Solution.swiftSwift Testing
ConsoleReady · runs in a sandboxed Swift container
Press Run to compile against the hidden test suite.