Swift Error Handling

Error handling lets your code respond gracefully when something goes wrong. Instead of silently failing or crashing, your program catches the problem and decides what to do next. Think of it like a car's warning system — when oil is low, the dashboard warns you instead of the engine stopping without notice.

Defining Errors

Create a custom error type by conforming to the Error protocol. An enum works perfectly because errors are a finite set of named cases.

enum NetworkError: Error {
    case noInternet
    case timeout
    case serverError(code: Int)
}

Throwing Errors

Mark a function with throws to indicate it might throw an error. Use throw to send an error when something goes wrong.

func fetchData(from url: String) throws -> String {
    guard url.hasPrefix("https") else {
        throw NetworkError.noInternet
    }
    return "Data from \(url)"
}

Calling Throwing Functions with do-try-catch

Wrap calls to throwing functions in a do block. Use try before the call, and handle errors in catch clauses.

do {
    let data = try fetchData(from: "http://example.com")
    print(data)
} catch NetworkError.noInternet {
    print("No internet connection.")
} catch NetworkError.timeout {
    print("Request timed out.")
} catch NetworkError.serverError(let code) {
    print("Server returned error code \(code).")
} catch {
    print("Unexpected error: \(error)")
}
// Output: No internet connection.

Diagram: Error Flow

fetchData(from: "http://...")
        |
     throws NetworkError.noInternet
        |
        v
   do { try fetchData() }
        |
   Error caught!
        |
   catch NetworkError.noInternet
        |
   print("No internet connection.")

Propagating Errors

A function can pass an error up the call chain instead of handling it itself. Mark the outer function with throws too.

func loadPage(url: String) throws -> String {
    let data = try fetchData(from: url)   // error propagates up
    return "Page content: \(data)"
}

do {
    let page = try loadPage(url: "http://bad-url.com")
    print(page)
} catch {
    print("Failed to load page: \(error)")
}
// Output: Failed to load page: noInternet

try? — Convert Error to Optional

Use try? to run a throwing function and get nil instead of an error on failure. Use this when you do not need to know the specific error.

let result = try? fetchData(from: "http://example.com")

if let data = result {
    print(data)
} else {
    print("Failed silently.")
}
// Output: Failed silently.

try! — Force Try (Risky)

Use try! only when you are certain the function will not throw. If it does throw, your app crashes immediately.

// Only safe if you are 100% sure this cannot fail
let data = try! fetchData(from: "https://example.com")
print(data)

Multiple Error Types

enum FileError: Error {
    case notFound(name: String)
    case permissionDenied
    case corrupt
}

func openFile(named filename: String) throws -> String {
    if filename.isEmpty {
        throw FileError.notFound(name: filename)
    }
    if filename == "secret.txt" {
        throw FileError.permissionDenied
    }
    return "Contents of \(filename)"
}

do {
    print(try openFile(named: "notes.txt"))
    print(try openFile(named: "secret.txt"))
} catch FileError.notFound(let name) {
    print("File '\(name)' not found.")
} catch FileError.permissionDenied {
    print("Access denied.")
} catch {
    print("Other error: \(error)")
}
// Output:
// Contents of notes.txt
// Access denied.

defer — Cleanup Code

A defer block runs when the current scope exits — whether it exits normally, through a return, or because of a thrown error. Use it to guarantee cleanup always happens.

func processFile(named name: String) throws {
    print("Opening file: \(name)")

    defer {
        print("Closing file: \(name)")   // Always runs
    }

    guard name != "bad.txt" else {
        throw FileError.corrupt
    }

    print("Processing file: \(name)")
}

try? processFile(named: "data.txt")
// Output:
// Opening file: data.txt
// Processing file: data.txt
// Closing file: data.txt

try? processFile(named: "bad.txt")
// Output:
// Opening file: bad.txt
// Closing file: bad.txt   ← defer still runs despite the throw

Result Type

Result<Success, Failure> is an alternative to throwing. It wraps either a success value or an error in a single enum. This is popular for async callbacks.

func divide(_ a: Int, by b: Int) -> Result<Int, Error> {
    guard b != 0 else {
        return .failure(NSError(domain: "Math", code: 1,
                                userInfo: [NSLocalizedDescriptionKey: "Division by zero"]))
    }
    return .success(a / b)
}

switch divide(10, by: 2) {
case .success(let value):
    print("Result: \(value)")
case .failure(let error):
    print("Error: \(error.localizedDescription)")
}
// Output: Result: 5

Summary

Swift error handling uses throws, throw, try, do, and catch to manage failure paths explicitly. Use try? to convert errors to nil and defer to guarantee cleanup. The Result type offers a clean alternative for async workflows. Together, these tools prevent silent crashes and make failure-handling a first-class part of your code.

Leave a Comment

Your email address will not be published. Required fields are marked *