Reading — step 1 of 7
Learn
Swift's error handling is typed and explicit — functions that throw declare it; callers must use try (or try? / try!); errors are values that conform to the Error protocol.
Defining errors
Any type conforming to Error can be thrown. Enums with associated values are ideal:
enum ParseError: Error {
case empty
case notANumber(String)
case negative(Int)
}
The Error protocol has no required methods — it's a marker.
Throwing functions
Mark with throws:
func parseAge(_ s: String) throws -> Int {
if s.isEmpty { throw ParseError.empty }
guard let n = Int(s) else { throw ParseError.notANumber(s) }
if n < 0 { throw ParseError.negative(n) }
return n
}
Callers MUST handle the possibility of failure — Swift won't let you ignore it.
try, try?, try!
Three ways to call a throwing function:
// 1. try inside do/catch — handle errors
do {
let age = try parseAge("42")
print(age)
} catch ParseError.empty {
print("empty input")
} catch ParseError.notANumber(let s) {
print("not a number: \(s)")
} catch ParseError.negative(let n) {
print("negative: \(n)")
} catch {
print("other error: \(error)")
}
// 2. try? — convert to optional (nil on error)
let age: Int? = try? parseAge("42")
// 3. try! — assert success (CRASH on error)
let definitelyValid = try! parseAge("42")
Use:
try+ do/catch when you want to handle the errortry?when you don't care WHY it failed, just whether it didtry!only when failure is genuinely impossible (constants, hardcoded values)
Pattern matching errors
The catch clause is a pattern — match specific cases:
do {
try doWork()
} catch ParseError.notANumber(let s) where s.hasPrefix("#") {
print("hex format not supported: \(s)")
} catch let error as ParseError {
print("parse error: \(error)")
} catch {
print("unknown: \(error)")
}
error is automatically bound in the default catch.
Propagating errors
A throwing function can let errors bubble up by simply marking itself throws:
func loadConfig(_ path: String) throws -> Config {
let text = try readFile(path) // readFile throws — propagates
return try parseConfig(text) // parseConfig throws — propagates
}
No need to do/catch — try without surrounding catch propagates. Cleaner than wrapping every call.
defer — guaranteed cleanup
func processFile(_ path: String) throws {
let file = try openFile(path)
defer {
file.close()
}
// process...
}
defer runs when the function exits, by ANY path — return, throw, or fallthrough. Like Go's defer or Java's try/finally.
rethrows
A function that calls a throwing closure can mark itself rethrows — only throws if the closure throws:
func transform<T>(_ items: [Int], using f: (Int) throws -> T) rethrows -> [T] {
return try items.map(f)
}
// Doesn't need try if f doesn't throw:
transform([1, 2, 3]) { $0 * 2 } // fine, no try
// Does if f throws:
try transform([1, 2, 3]) { try parseAge("\($0)") }
Common for higher-order functions like map, filter, forEach.
When to use error throwing vs Result
Swift has both throws and Result<Success, Failure>. Use:
throwsfor the common case — synchronous, straightforward control flowResultwhen you need to STORE the result for later (e.g., async callbacks, queues), or when you want to compose with map/flatMap chain
They convert easily: Result(catching: { try parseAge(s) }) and try result.get().
Common mistakes
- Using
try!reflexively — turns recoverable errors into crashes. Use only for true invariants. - Catching
Erroras a wide net — loses type information. Match specific cases when you can react meaningfully. - Forgetting that
throwsis part of the function type — closures that throw needthrowsin their type signature. - Throwing String —
throw "oops"— String doesn't conform to Error. Define an enum or use NSError. - Mixing thrown errors with Optional-returns — pick one. throws conveys 'why it failed'; optional says 'it failed or didn't'.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…