Skip to content

Error Handling

Swift provides a robust error-handling system using throw, try, and catch to manage recoverable errors, ensuring code safety and clarity. Errors represent exceptional conditions, such as invalid input or network failures.

Defining Errors

Errors conform to the Error protocol, typically implemented as enums for specific cases.

Example: Basic Error Enum:

swift
enum NetworkError: Error {
    case noConnection
    case invalidURL(String)
    case serverError(statusCode: Int)
}

enum ValidationError: Error {
    case emptyInput
    case invalidFormat(String)
}

Example: Custom Error with Details:

swift
struct FileError: Error, CustomStringConvertible {
    let code: Int
    let message: String
    
    var description: String {
        return "FileError(\(code): \(message))"
    }
}

Throwing Errors

Functions that can throw errors are marked with throws.

Example: Throwing Function:

swift
func fetchData(from urlString: String) throws -> Data {
    guard urlString.contains("http") else {
        throw NetworkError.invalidURL(urlString)
    }
    guard !NetworkManager.isOffline else {
        throw NetworkError.noConnection
    }
    // Simulate fetch
    return Data()
}

Example: Throwing Initializer:

swift
struct Document {
    let content: String
    init(content: String) throws {
        guard !content.isEmpty else {
            throw ValidationError.emptyInput
        }
        self.content = content
    }
}

Handling Errors

Use try to call throwing functions within a do-catch block, or use try?/try! for alternative handling.

Do-Catch Blocks

Catch specific or general errors.

Example: Specific Error Handling:

swift
do {
    let data = try fetchData(from: "invalid")
    print("Data fetched: \(data.count) bytes")
} catch NetworkError.invalidURL(let url) {
    print("Invalid URL: \(url)")
} catch NetworkError.noConnection {
    print("No network connection")
} catch {
    print("Unexpected error: \(error)")
}

Example: Nested Error Handling:

swift
func processFile(_ path: String) throws -> String {
    do {
        let doc = try Document(path: path)
        return doc.content.uppercased()
    } catch ValidationError.emptyInput {
        throw FileError(code: 400, message: "Empty file")
    }
}

do {
    let result = try processFile("")
    print(result)
} catch let fileError as FileError {
    print(fileError.description)
} catch {
    print(error)
}

Try?

Converts errors to nil.

Example:

swift
let optionalData = try? fetchData(from: "invalid")
print(optionalData ?? "No data") // "No data"

Try!

Forces execution, crashing on errors.

Example:

swift
// let data = try! fetchData("invalid") // Crashes on error

Propagating Errors

Rethrow errors in throwing functions.

Example:

swift
func executeRequest(_ url: String) throws -> Data {
    return try fetchData(from: url)
}

Defer Statements

Execute cleanup code regardless of success or failure.

Example:

swift
func processResource() throws {
    let resource = acquireResource()
    defer { releaseResource(resource) }
    guard isValid(resource) else {
        throw ResourceError.invalid
    }
    // Process resource
}

Localized Error Descriptions

Conform to LocalizedError for user-friendly messages.

Example:

swift
extension NetworkError: LocalizedError {
    var errorDescription: String? {
        switch self {
        case .noConnection:
            return NSLocalizedString("No internet connection.", comment: "")
        case .invalidURL(let url):
            return NSLocalizedString("Invalid URL: \(url)", comment: "")
        case .serverError(let code):
            return NSLocalizedString("Server error with code \(code)", comment: "")
        }
    }
}

do {
    try fetchData(from: "invalid")
} catch {
    print(error.localizedDescription) // "Invalid URL: invalid"
}

Error Recovery Strategies

Implement retry or fallback logic.

Example:

swift
func fetchWithRetry(url: String, retries: Int = 3) throws -> Data {
    var lastError: Error?
    for attempt in 1...retries {
        do {
            return try fetchData(from: url)
        } catch {
            lastError = error
            print("Attempt \(attempt) failed: \(error)")
            if attempt == retries { throw error }
            Thread.sleep(forTimeInterval: 1.0)
        }
    }
    throw lastError ?? NetworkError.noConnection
}

Best Practices

  • Specific Errors: Use enums for granular error types.
  • Localized Errors: Provide user-friendly messages.
  • Defer Cleanup: Ensure resources are released.
  • Handle Appropriately: Catch errors at the right level.
  • Avoid Try!: Use only in guaranteed-safe cases.
  • Retry Logic: Implement for transient errors (e.g., network).
  • Document Throws: Specify error types in documentation.

Troubleshooting

  • Missing Catch: Ensure all errors are caught or propagated.
  • Unexpected Error: Cast to specific types for debugging.
  • Resource Leaks: Verify defer statements execute.
  • Retry Failures: Log errors for each attempt.
  • Localization Issues: Test localized strings in multiple languages.

Example: Comprehensive Error Handling

swift
enum DatabaseError: Error, LocalizedError {
    case notFound(id: String)
    case connectionFailed
    case invalidQuery(String)
    
    var errorDescription: String? {
        switch self {
        case .notFound(let id):
            return "Record with ID \(id) not found."
        case .connectionFailed:
            return "Database connection failed."
        case .invalidQuery(let query):
            return "Invalid query: \(query)."
        }
    }
}

class Database {
    private var isConnected = false
    
    func connect() throws {
        defer { print("Cleaning up connection attempt") }
        guard canConnect() else {
            throw DatabaseError.connectionFailed
        }
        isConnected = true
    }
    
    func query(_ sql: String) throws -> [String: Any] {
        guard isConnected else {
            throw DatabaseError.connectionFailed
        }
        guard isValidQuery(sql) else {
            throw DatabaseError.invalidQuery(sql)
        }
        // Simulate query
        return ["id": "123", "name": "Alice"]
    }
}

func executeQuery(_ db: Database, sql: String) throws -> [String: Any] {
    do {
        try db.connect()
        return try db.query(sql)
    } catch DatabaseError.connectionFailed {
        print("Retrying connection...")
        try db.connect()
        return try db.query(sql)
    }
}

let db = Database()
do {
    let result = try executeQuery(db, "SELECT * FROM users")
    print(result)
} catch {
    print(error.localizedDescription)
}

Released under the MIT License.