Throwing and catching errors
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 yourdo/catchor out of athrowsfunction.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.
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.
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.
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.