Swift Error Propagation
Error propagation is letting an error travel from a throwing call to the caller instead of handling it where it happens. The caller then decides how to handle the error.
An error travels up two levels
The readText function throws the missing case. The loadText function calls it with a bare try and no catch, so the error travels to the do block at the top.
enum FileError: Error {
case missing
case empty
}
func readText() throws -> String {
throw FileError.missing
}
func loadText() throws -> String {
let text = try readText()
return text
}
do {
let text = try loadText()
print(text)
} catch {
print(error)
}A value travels up the same path
The parse function turns text into a number and throws for text that is not a number. The double function passes the call through, and the working value reaches the do block.
enum CodeError: Error {
case notNumber
}
func parse(text: String) throws -> Int {
if let number = Int(text) {
return number
}
throw CodeError.notNumber
}
func double(text: String) throws -> Int {
let number = try parse(text: text)
return number * 2
}
do {
let value = try double(text: "21")
print(value)
} catch {
print(error)
}The middle function replaces the error
Here the middle function does catch the error from readFile. It throws its own unavailable case instead, and the top catch prints that case name.
enum FileError: Error {
case missing
}
enum LoadError: Error {
case unavailable
}
func readFile() throws -> String {
throw FileError.missing
}
func load() throws -> String {
do {
let text = try readFile()
return text
} catch {
throw LoadError.unavailable
}
}
do {
let text = try load()
print(text)
} catch {
print(error)
}Example
enum SignupError: Error {
case tooYoung
}
func checkAge(_ age: Int) throws {
if age < 18 {
throw SignupError.tooYoung
}
}
func register(age: Int) throws -> String {
try checkAge(age)
return "Registered"
}
do {
let text = try register(age: 20)
print(text)
} catch {
print(error)
}
do {
let text = try register(age: 15)
print(text)
} catch {
print(error)
}Key points
- A throwing function can pass an error to its own caller instead of catching it.
- The function that passes an error on must itself be marked throws.
- A try call inside a throws function needs no do block when the error is left to travel.
- The error keeps traveling until a caller catches it with a do catch.
- A middle function can catch an error and throw a different error in its place.
- Printing a passed error prints its case name.
In this example, the checkAge function throws tooYoung when the age is under 18. The register function is marked throws and calls checkAge with a bare try, so it catches nothing itself. The first do block registers age 20 and prints Registered. The second do block registers age 15, the error travels through register, and the catch prints tooYoung.
Try it Yourself
Edit the code, then press Run.
Running is simulated
Exercise
A throws function calls another throwing function with a bare try and has no catch of its own. What happens when the inner call throws?