Skip to main content

Handling Errors

1. Defining Error Types

Error Type Description Usage
Error Protocol defining the basic structure of an error Implement to create custom errors
NSError Foundation's error type, bridged to Swift's Error Used for interoperability with Objective-C APIs
Swift.Error Swift's native error protocol, adopted by all error types Primary way to handle errors in Swift code

Example: Custom Error Type

enum NetworkError: Error {
    case noConnection
    case timeout
    case invalidURL
}

Example: Error with Associated Values

enum FileError: Error {
    case fileNotFound(String)
    case permissionDenied(String)
}

2. Throwing Errors

Method Description Example
throw Used to throw an error from a function or method
func throwError() throws {
    throw MyError.invalidInput
}
rethrows Indicates a function that can throw only if the passed closure throws
func processItems(_ items: [Int], operation: (Int) throws -> Bool) rethrows {
    try items.first { item in
        return operation(item)
    }
}

Example: Throwing Errors in a Function

enum NetworkError: Error {
    case noConnection
    case timeout
}

func fetchData() throws -> String {
    // Simulate network call that might fail
    if Bool.random() {
        throw NetworkError.noConnection
    }
    return "Data received"
}
Note: The throws keyword in a function signature indicates that the function can throw an error, and any caller must handle it using try, try?, or try!.

3. Using Throwing Functions

Function Description Throws
func readFile(_ path: String) throws -> String Reads content from a file at the given path FileError
func saveFile(_ data: Data, to path: String) throws Saves data to a file at the specified path FileError
func fetchJSON(from url: URL) throws -> [String: Any] Fetches JSON data from a URL NetworkError

Example: Using Throwing Functions

func readAndProcessFile(path: String) throws {
    let content = try readFile(path)
    let processed = process(content)
    try saveFile(processed, to: "output.txt")
}

do {
    try readAndProcessFile(path: "input.txt")
} catch FileError.missing {
    print("File not found")
} catch NetworkError.timeout {
    print("Request timed out")
}

4. Catching Errors

Error Handling Method Description Usage
do-catch Primary Swift error handling construct. Used to handle errors thrown by functions.
do {
    try someThrowingFunction()
} catch {
    print("An error occurred: \(error)")
}
catch with pattern matching Allows catching specific error types or conditions using pattern matching.
do {
    try someThrowingFunction()
} catch NetworkError.noConnection {
    print("No internet connection")
} catch let error as NSError {
    print("An NSError occurred: \(error)")
}
catch with multiple patterns Handle multiple error types in a single catch block.
do {
    try someThrowingFunction()
} catch NetworkError.noConnection, NetworkError.timeout {
    print("Network issue occurred")
}

Example: Throwing and Catching Errors

enum NetworkError: Error {
    case noConnection
    case timeout
    case invalidURL
}

func fetchData() throws -> String {
    // Simulate network error
    throw NetworkError.noConnection
}

do {
    let data = try fetchData()
    print(data)
} catch NetworkError.noConnection {
    print("No internet connection")
} catch NetworkError.timeout {
    print("Request timed out")
} catch {
    print("An unknown error occurred: \(error)")
}
Note: The try keyword is required before any function that can throw an error.
Warning: If you do not handle an error with do-catch, your app will crash.

5. Converting Errors to Optionals

Method Description Example
try? Converts a throwing expression to an optional value let file = try? String(contentsOfFile: "example.txt")
try! Converts a throwing expression to a non-optional value let file = try! String(contentsOfFile: "example.txt")
Note: try? returns nil if an error occurs, while try! will crash if an error is thrown.

6. Disabling Error Propagation

Approach Description Code Example
try! Forces unwrapping of a throwing expression, crashes if error occurs let file = try! FileHandle(forReadingFrom: url)
try? Converts a throwing expression into an optional, returns nil if error occurs let file = try? FileHandle(forReadingFrom: url)
Note: try! should be used sparingly as it can cause runtime crashes if not handled properly.

7. Using Defer for Cleanup

The defer statement in Swift allows you to execute code just before the current scope exits. It's particularly useful for cleanup operations like closing files, releasing resources, or resetting state.

Feature Description Example
defer Ensures that a block of code is executed before the current scope exits.
func readFile() {
    let file = openFile("data.txt")
    defer {
        closeFile(file)
    }
    // Read from file...
}

Example: Resource Management with defer

func processFile() {
    let file = openFile("input.txt")
    defer {
        closeFile(file)
    }
    
    let data = readFile(file)
    process(data)
}
Note: defer statements are executed in LIFO (Last In, First Out) order. The last defer block is executed first when exiting the scope.

Key Points

  • defer ensures cleanup code runs even if an error occurs.
  • Multiple defer statements are executed in reverse order of declaration.
  • Use defer for managing resources like file handles, network connections, or locks.

8. Creating Custom Error Types

Property Description Example
Error Protocol Swift's Error protocol is the base for all error types struct MyError: Error { }
Error Cases Custom errors can have multiple cases with associated values enum NetworkError: Error { case timeout; case invalidURL(URL) }
Localized Error Implement localizedDescription for user-friendly messages extension MyError: LocalizedError { var errorDescription: String? { "Something went wrong" } }

Example: Custom Error with Associated Values

enum FileError: Error {
    case fileNotFound(String)
    case permissionDenied(String)
    case corruptedFile(String)
}

extension FileError: LocalizedError {
    var errorDescription: String? {
        switch self {
        case .fileNotFound(let fileName):
            return "File not found: \(fileName)"
        case .permissionDenied(let fileName):
            return "Permission denied for file: \(fileName)"
        case .corruptedFile(let fileName):
            return "File corrupted: \(fileName)"
        }
    }
}

Example: Throwing and Catching Custom Errors

func readFile(_ fileName: String) throws -> String {
    if fileName.isEmpty {
        throw FileError.fileNotFound(fileName)
    }
    // Simulate file reading logic
    return "File content"
}

do {
    let content = try readFile("example.txt")
    print(content)
} catch FileError.fileNotFound(let fileName) {
    print("Caught file not found error: \(fileName)")
} catch {
    print("An error occurred: \(error.localizedDescription)")
}

9. Rethrowing Functions

Function Description Notes
func rethrowingFunction() throws -> String Declares a function that can rethrow errors from a nested call Must be marked with throws in signature
func handleAndRethrow() throws -> String Handles error internally and rethrows it using throw Used to transform or log errors before rethrowing

Example: Rethrowing a Thrown Error

func fetchData() throws -> String {
    guard let data = try? String(contentsOfFile: "file.txt") else {
        throw NSError(domain: "FileError", code: 1, userInfo: nil)
    }
    return data
}

func processAndRethrow() throws -> String {
    do {
        let content = try fetchData()
        return content
    } catch {
        print("Error caught and rethrown: \(error)")
        throw error // Rethrowing the original error
    }
}

Example: Using Rethrowing in Higher-Order Functions

func processItems(_ items: [String], transform: (String) throws -> String) throws -> [String] {
    return try items.map { item in
        try transform(item)
    }
}

func safeTransform(_ input: String) throws -> String {
    guard !input.isEmpty else {
        throw NSError(domain: "InputError", code: 2, userInfo: nil)
    }
    return input.uppercased()
}

let items = ["hello", "world"]
let result = try processItems(items, transform: safeTransform)

10. Understanding Error Propagation Patterns

Pattern Description Usage
Throw and Catch Errors are thrown using throw and caught with do-catch blocks. Used for handling expected errors in a structured way.
Error Propagation Errors can be propagated up the call stack using try without catch. When a function can't handle an error, it lets the caller deal with it.
Result Type Uses Result<Success, Error> to wrap success or failure values. Alternative to throwing errors, useful for functional programming styles.
Optional Chaining with Error Handling Errors can be handled using optional chaining and nil values. Used in cases where a value might not be present or an operation fails.

Example: Throw and Catch

enum NetworkError: Error {
    case noConnection
    case timeout
}

func fetchData() throws -> String {
    // Simulate a network call that might fail
    throw NetworkError.noConnection
}

do {
    let data = try fetchData()
    print(data)
} catch NetworkError.noConnection {
    print("No internet connection")
} catch {
    print("An error occurred: \(error)")
}

Example: Result Type

enum DataFetchResult {
    case success(String)
    case failure(Error)
}

func fetchWithResult() -> DataFetchResult {
    return .failure(NetworkError.timeout)
}

let result = fetchWithResult()
switch result {
case .success(let data):
    print("Data: \(data)")
case .failure(let error):
    print("Error: \(error)")
}

Was this article helpful?