Error Handling in Swift: try/catch, throws, and do-catch Blocks

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Error Handling in Swift: try/catch and Beyond



Developing for iOS and macOS requires reliable error management. Swift provides a powerful and elegant error handling mechanism based on the keywords throws, try, catch, and do. In this article, we will break down how to properly handle errors, write readable code, and avoid common pitfalls.



1. Basics: The Error Protocol and the throws Keyword



Any error in Swift must conform to the Error protocol. Typically, enumerations (enums) are used for this:



enum NetworkError: Error {    case badURL    case noData    case decodingFailed(String)}


A function that can throw an error is marked with the throws keyword:



func fetchUser(from urlString: String) throws -> User {    guard let url = URL(string: urlString) else {        throw NetworkError.badURL    }    // ... request logic}


Now, when calling such a function, the compiler will require you to handle the potential error.



2. The do-catch Construct: Catching Errors



The most common way to handle errors is the do-catch block. You place potentially dangerous code in the do block, and handle the failure in the catch block:



do {    let user = try fetchUser(from: "https://api.example.com/user")    print("User loaded: \\(user.name)")} catch NetworkError.badURL {    print("Invalid URL")} catch NetworkError.noData {    print("Server returned no data")} catch NetworkError.decodingFailed(let message) {    print("Decoding error: \\(message)")} catch {    print("Unknown error: \\(error.localizedDescription)")}


Important: the last catch block without a specific type catches all remaining errors — you should always include it.



2.1. Pattern Matching in catch



You can use more complex conditions:



do {    let result = try riskyOperation()} catch let error as NetworkError where error == .badURL {    print("Problem with URL")} catch {    print(error)}


3. Alternatives: try? and try!



Full error handling is not always necessary. Swift offers two simplified approaches:



3.1. try? — Optional Value



If the error is not critical, you can convert the result into an optional. On failure, the function returns nil:



let user = try? fetchUser(from: "https://example.com/user")// user has type User? — nil on error


Ideal for situations where the error can simply be ignored (e.g., loading an avatar).



3.2. try! — Forced Execution



Use try! only when you are absolutely certain that no error will occur. Otherwise, the app will crash:



let user = try! fetchUser(from: "https://valid-url.com/user")// If the URL is invalid — the app will crash


Apply try! in tests or in situations where a failure is logically impossible.



4. Advanced Techniques: rethrows and deferred



4.1. rethrows for Higher-Order Functions



If your function accepts a closure that can throw an error, use rethrows. This allows you not to mark your own function as throws if the closure does not throw an error:



func process(_ closure: () throws -> Void) rethrows {    try closure()}

// Call without a throws-closure — does not require do-catchprocess { print("No errors") }

// Call with a throws-closure — requires handlingtry process { throw NetworkError.noData }


4.2. deferred: Guaranteed Cleanup



The defer block executes upon any function exit (success or error). It is perfect for closing files or releasing resources.

Blogs

Book Recommendations