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:
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:
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, andthrowlike exceptions in other languages but avoids costly stack unwinding, makingthrowas efficient asreturn.
Propagating Errors with Throwing Functions
Mark functions that can throw errors with throws after parameters:
func canThrowErrors() throws -> String
func cannotThrowErrors() -> StringThrowing 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:
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:):
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:
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:
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:
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:
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:
func someThrowingFunction() throws -> Int {
// ...
}
let x = try? someThrowingFunction() // x is Int? ; nil on errorFor chained attempts:
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):
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:
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:
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.):
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.