Error Handling in Swift: try/catch and Beyond — Advanced Level
Error handling is an integral part of any serious iOS/macOS development. Swift provides a powerful and expressive error management system that goes far beyond simple try/catch. In this article, we will explore advanced techniques: working with the Error protocol, error enums, the throws and rethrows keywords, as well as the Result type and functional approaches. The material is intended for developers already familiar with the basics of Swift.
1. The Error Protocol and Custom Error Types
At the core of error handling in Swift is the Error protocol. Any type that conforms to this protocol can be thrown as an error. Most often, enums (enum) are used for this purpose.
import Foundation
// Define a custom error type for a network requestenum NetworkError: Error { case invalidURL case noData case decodingFailed(description: String) case serverError(statusCode: Int)}
// A function that can throw an errorfunc fetchUserData(from urlString: String) throws -> Data { guard let url = URL(string: urlString) else { throw NetworkError.invalidURL }
// Simulate a network request let success = Bool.random() if !success { throw NetworkError.serverError(statusCode: 500) }
return Data()}Using enums allows you to group related errors and pass additional information (associated values), making the code more readable and self-documenting.
2. Advanced Use of do-catch
The do-catch block is the primary tool for catching errors. At an advanced level, it is important to be able to handle different types of errors differently.
func processUserData() { do { let data = try fetchUserData(from: "https://api.example.com/user") // Work with the data print("Data received: \\(data.count) bytes") } catch NetworkError.invalidURL { print("Error: invalid URL") } catch NetworkError.serverError(let code) where code >= 500 { print("Server error with code \\(code). Please try again later.") } catch let error as NetworkError { print("Network error: \\(error)") } catch { // Handle all other errors print("Unknown error: \\(error.localizedDescription)") }}Key points:
- Pattern matching with
where: allows filtering errors by additional conditions (e.g., a range of response codes). - Type casting (
as): if you expect multiple error types, you can usecatch let error as MyErrorType. - Generic
catch: should always be last, as it catches any unhandled error.
3. The throws, rethrows Keywords and Propagating Errors
The throws keyword indicates that a function can throw an error. rethrows is a more subtle mechanism: it states that a function can throw an error only if one of its closure parameters throws an error.
// Function with throws — can throw an error itselffunc loadConfig() throws -> [String: String] { // ... throw NetworkError.noData}
// Function with rethrows — propagates an error from a closurefunc performWithRetry(times: Int, operation: () throws -> Void) rethrows { for _ in 0..<times { try operation() }}
// Usagedo { try performWithRetry(times: 3) { try loadConfig() }} catch { print("Operation failed after 3 attempts: \\(erro