Errors, memory and semantics · core
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?
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.