Error Handling
Error handling allows your program to safely respond to and recover from unexpected failures instead of crashing abruptly.
Defining Error Types
In Swift, errors are represented using types that conform to the Error protocol. Enums are the most common and effective way to group related error conditions because they allow you to safely model exclusive states. You can even attach associated values to provide extra context about the failure.
enum PrinterError: Error {
case outOfPaper
case noToner
case jammed(paperCount: Int) // Associated value provides more detail
case onFire
}Throwing Errors
Use the throws keyword right before the return type in a function's declaration to indicate it can fail. When a failure condition is met, use the throw keyword to toss the error.
func send(job: Int, toPrinter printerName: String) throws -> String {
if printerName == "Broken" {
throw PrinterError.onFire
} else if printerName == "Jammed" {
throw PrinterError.jammed(paperCount: 5)
}
return "Job sent successfully"
}Handling Errors (do-catch)
Wrap your throwing functions inside a do block and use the try keyword to call them. You can safely call multiple throwing functions inside a single do block. If any of them fail, the execution immediately jumps out of the do block and down to the matching catch block.
You can catch specific errors, bind associated values to read details, or use catch let to bind the error to a custom variable name.
do {
// You can stack multiple try statements in a single block
let setup = try preparePrinter() // Assume this is another throwing function
let response = try send(job: 10, toPrinter: "Jammed")
print(response) // This line is skipped if anything above throws an error
} catch PrinterError.onFire {
print("Call the fire department!")
} catch PrinterError.jammed(let paperCount) {
// Catch with binding to read the associated value
print("Oh no, \(paperCount) pages are jammed.")
} catch let e as PrinterError {
// Catch with binding to cast or assign the error to a custom variable
print("Printer error occurred: \(e)")
} catch {
// A default catch-all block to catch any unhandled errors
// "error" is automatically available — it's the thrown value
print("An unexpected error occurred: \(error)")
}Rethrows
Sometimes you write a function that accepts a closure as an argument. If that closure throws an error, you want your main function to pass that error along to the caller.
You use the rethrows keyword for this. It tells the compiler that the function itself only throws an error if the provided closure throws one.
func executeTask(task: () throws -> Void) rethrows {
print("Preparing to execute...")
try task() // If this throws, the error is safely passed up the chain
print("Execution complete.")
}Optional Errors (try?)
Use try? to handle an error by converting the result into an optional value. If the function succeeds, you get an optional containing the result. If it throws an error, the error is completely ignored and you simply get nil.
let response = try? send(job: 10, toPrinter: "Broken")
// response is nil, and the app continues running safelyDisabling Error Propagation (try!)
Use try! only when you are absolutely certain a function will not throw an error at runtime. This forces the result to unwrap automatically, but if an error actually does occur, your app will immediately crash.
let response = try! send(job: 10, toPrinter: "Office")