Skip to content

Error Handling

Error handling involves responding to and recovering from errors in your code. Swift offers built-in support for throwing, catching, propagating, and managing recoverable errors during runtime.

Certain operations may not always succeed or yield valid results. While optionals handle missing values, understanding the cause of a failure helps your code respond effectively.

For instance, reading data from a file can fail if the file doesn't exist, lacks read permissions, or uses an incompatible format. Identifying these issues allows your program to fix some problems or notify the user about others.

Note: Swift's error handling works with Cocoa and Objective-C patterns using NSError. For details, refer to relevant Apple documentation.

Representing and Throwing Errors

Errors in Swift are types conforming to the Error protocol, marking them as usable for error handling.

Enumerations work well for grouping related errors, with associated values for extra details. Here's an example for a book library system:

swift
enum BookLibraryError: Error {
    case invalidBook
    case insufficientBalance(amountNeeded: Int)
    case outOfCopies
}

Throw an error with the throw statement to signal an issue and halt normal execution. For example:

swift
throw BookLibraryError.insufficientBalance(amountNeeded: 5)

Handling Errors

When an error is thrown, surrounding code must handle it—by fixing it, trying another way, or alerting the user.

Swift provides four handling methods: propagate to the caller, use do-catch, treat as an optional, or assert no error occurs. Use try, try?, or try! before calls that might throw errors.

Note: Swift's error handling uses try, catch, and throw like exceptions in other languages but avoids costly stack unwinding, making throw as efficient as return.

Propagating Errors with Throwing Functions

Mark functions that can throw errors with throws after parameters:

swift
func canThrowErrors() throws -> String
func cannotThrowErrors() -> String

Throwing functions pass errors to their callers. Non-throwing functions must handle errors internally.

In this book library example, the Library class's borrow(bookNamed:) method throws errors if the book is invalid, out of copies, or costs more than the balance:

swift
struct Book {
    var price: Int
    var copies: Int
}

class Library {
    var catalog = [
        "Novel": Book(price: 15, count: 5),
        "Textbook": Book(price: 20, count: 3),
        "Magazine": Book(price: 5, count: 10)
    ]
    var balance = 0

    func borrow(bookNamed name: String) throws {
        guard let book = catalog[name] else {
            throw BookLibraryError.invalidBook
        }

        guard book.copies > 0 else {
            throw BookLibraryError.outOfCopies
        }

        guard book.price <= balance else {
            throw BookLibraryError.insufficientBalance(amountNeeded: book.price - balance)
        }

        balance -= book.price

        var updatedBook = book
        updatedBook.copies -= 1
        catalog[name] = updatedBook

        print("Borrowing \(name)")
    }
}

The borrowFavoriteBook(person:library:) function propagates errors from borrow(bookNamed:):

swift
let favoriteBooks = [
    "Alice": "Novel",
    "Bob": "Textbook",
    "Eve": "Magazine",
]

func borrowFavoriteBook(person: String, library: Library) throws {
    let bookName = favoriteBooks[person] ?? "Novel"
    try library.borrow(bookNamed: bookName)
}

Throwing initializers propagate errors similarly:

swift
struct BorrowedBook {
    let name: String
    init(name: String, library: Library) throws {
        try library.borrow(bookNamed: name)
        self.name = name
    }
}

Handling Errors with Do-Catch

Use do-catch to run code and catch errors:

swift
do {
    try // expression
    // statements
} catch // pattern 1 {
    // statements
} catch // pattern 2 where // condition {
    // statements
} catch {
    // statements
}

A catch without a pattern binds any error to error. Example:

swift
var library = Library()
library.balance = 10
do {
    try borrowFavoriteBook(person: "Alice", library: library)
    print("Success! Enjoy.")
} catch BookLibraryError.invalidBook {
    print("Invalid Book.")
} catch BookLibraryError.outOfCopies {
    print("Out of Copies.")
} catch BookLibraryError.insufficientBalance(let amountNeeded) {
    print("Insufficient balance. Add \(amountNeeded) more.")
} catch {
    print("Unexpected error: \(error).")
}
// Prints "Insufficient balance. Add 5 more." (if balance is low)

Propagate unhandled errors. Catch multiple errors in one clause:

swift
func getBook(named: String) throws {
    do {
        try library.borrow(bookNamed: named)
    } catch BookLibraryError.invalidBook, BookLibraryError.insufficientBalance, BookLibraryError.outOfCopies {
        print("Issue with book: invalid, low balance, or no copies.")
    }
}

Converting Errors to Optionals

Use try? to turn errors into nil:

swift
func someThrowingFunction() throws -> Int {
    // ...
}

let x = try? someThrowingFunction()  // x is Int? ; nil on error

For chained attempts:

swift
func fetchData() -> Data? {
    if let data = try? fetchFromLocal() { return data }
    if let data = try? fetchFromRemote() { return data }
    return nil
}

Disabling Error Propagation

Use try! when certain no error will occur (runtime crash if wrong):

swift
let image = try! loadImage(path: "./assets/default.jpg")

Specifying the Error Type

Specify error types with throws(ErrorType) for constrained scenarios, like embedded systems or internal libraries. Example:

swift
enum StatsError: Error {
    case noData
    case invalidValue(Int)
}

func summarize(values: [Int]) throws(StatsError) {
    guard !values.isEmpty else { throw .noData }

    var counts = [1: 0, 2: 0, 3: 0]
    for value in values {
        guard value > 0 && value <= 3 else { throw .invalidValue(value) }
        counts[value]! += 1
    }

    print("*", counts[1]!, "-- **", counts[2]!, "-- ***", counts[3]!)
}

Call from broader functions:

swift
func process() throws {
    let values = [1, 2, 3]
    try summarize(values: values)
}

Use throws(Never) for non-throwing functions. In do-catch, specify types for exhaustive handling.

Cleanup with Defer

Use defer for actions before exiting a scope, regardless of how (error, return, etc.):

swift
func processFile(filename: String) throws {
    if exists(filename) {
        let file = open(filename)
        defer { close(file) }
        while let line = try file.readLine() {
            // Process line
        }
    }
}

Defers run in reverse order of appearance.

Released under the MIT License.