Error handling
Swift's throws is neither exceptions nor error codes — it is a second return path the compiler tracks.
By the end you will be able to
- Define error types and throw them from functions
- Handle errors with do/catch, try?, try! and defer, and pick the right one
- Explain how throws differs from exceptions in Java or C++
A function is declared func load() throws -> Data. What happens if you call it as let data = load()?
Defining errors
Any type conforming to Error can be thrown. Enums are the usual choice, because the cases enumerate exactly what can go wrong:
enum LoadError: Error {
case notFound(path: String)
case permissionDenied
case corrupted(reason: String)
}
Throwing and calling
func load(_ path: String) throws -> String {
guard path.hasPrefix("/") else {
throw LoadError.notFound(path: path)
}
return "contents of \(path)"
}
do {
let text = try load("/etc/hosts")
print(text)
} catch LoadError.notFound(let path) {
print("no file at \(path)")
} catch {
print("something else: \(error)")
}
A bare catch binds the error to a constant called error automatically.
The three flavours of try
| Form | Meaning | When |
|---|---|---|
try | propagate the error to my caller | inside a throws function, or a do block |
try? | turn the error into nil | you genuinely do not care why it failed |
try! | crash if it throws | you can state why this cannot fail |
let a = try load(path) // I will handle or propagate it
let b = try? load(path) // String? — nil on any failure
let c = try! load("/etc/hosts") // trust me
try? is convenient and dangerous in the same way ! is on optionals: it discards the reason. Use it when a failure genuinely has one obvious response, not to make a compile error go away.
How this differs from exceptions
In C++ (and with Java's unchecked exceptions), an exception can be thrown from anywhere and propagate through functions that know nothing about it. That makes any line a potential exit point, and it is why exception-safe code is hard to write. (Java's checked exceptions do appear in method signatures — but even there, nothing marks the call sites.)
Swift's model is narrower on purpose:
- A function that can fail must say
throwsin its signature. - Every call to one must be marked
try— visible at the call site. - Errors do not unwind arbitrary stack frames;
throwsis effectively a second return value the compiler threads through for you. - There is no
finally—deferdoes that job, and works everywhere, not just around errors.
The practical result: reading a function, you can see every line that can fail because each one starts with try.
void process() {
var data = load(); // might throw
var parsed = parse(data); // might throw
save(parsed); // might throw
}
// Nothing in the body says so.func process() throws {
let data = try load()
let parsed = try parse(data)
try save(parsed)
}
// Every failure point is marked.Same three calls. In the Swift version the failure points are impossible to miss, and the signature tells every caller that this function can fail.
defer — cleanup that always runs
func process(_ path: String) throws {
let handle = open(path)
defer { close(handle) } // runs on every exit path
try validate(handle) // if this throws, close still happens
try transform(handle)
}
defer blocks run when the current scope exits — by return, by throw, or by falling off the end — in reverse order of declaration. This is Swift's finally, except it is attached to the resource rather than to a try block, so the acquisition and release sit next to each other.
Typed throws
Swift 6 lets a function declare exactly which error type it can throw:
func load(_ path: String) throws(LoadError) -> String
Callers then get an exhaustive catch — the compiler knows the full set of cases. Use it for small, closed error domains inside a module. For public API that may grow new failure modes, plain throws stays more flexible, since adding a case to a typed-throws error is a source-breaking change for every caller.
defer runs on the way out, whatever the exit path. What does this print?
enum E: Error { case boom }
func work() throws -> Int {
defer { print("first") }
defer { print("second") }
print("body")
throw E.boom
}
do {
print(try work())
} catch {
print("caught")
}When is try? the right choice?
Throw and catch
Write withdraw(_:from:) that throws .insufficientFunds when the amount exceeds the balance and .invalidAmount when it is not positive. Print one line per attempt.
Clean up with defer
Add logging that runs whether or not the parse succeeds, and convert failures to nil at the call site.
Errors, optionals and exhaustive switches are all Swift making a failure impossible to ignore. Write a paragraph on what these three have in common — and one place where Swift deliberately lets you opt out of the safety, and why that escape hatch exists.
You can now:
- Define error enums and throw them
- Handle errors with
do/catch,try?,try!and pattern-matched catch clauses - Use
deferfor cleanup on every exit path - Explain how
throwsdiffers from exceptions
Next up: ARC — how Swift frees memory, and the one situation where it needs your help.