Skip to content

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.

swift
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.

swift
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.

swift
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.

swift
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.

swift
let response = try? send(job: 10, toPrinter: "Broken")
// response is nil, and the app continues running safely

Disabling 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.

swift
let response = try! send(job: 10, toPrinter: "Office")