Error Handling in Swift: try/catch and Beyond (Advanced Level)
\n\nError handling is one of the key topics in Swift development. If you are already familiar with basic do-catch and try?, it's time to move to the next level. In this article, we will cover advanced techniques: working with the Error protocol, the Result type, the rethrows keyword, and how to properly design error hierarchies in real iOS/macOS projects.
Deep Dive into the Error Protocol and Custom Errors
\n\nIn Swift, any error must conform to the Error protocol. However, to make code more readable and maintainable, it's worth creating custom enumerations (enum) with nested cases and associated values.
enum NetworkError: Error {\n case invalidURL\n case noData\n case decodingFailed(description: String)\n case serverError(statusCode: Int)\n case timeout(seconds: Double)\n}\n\n// Usage example\nfunc fetchUser(id: Int) throws -> User {\n guard id > 0 else {\n throw NetworkError.invalidURL\n }\n // ... request logic\n if someCondition {\n throw NetworkError.serverError(statusCode: 500)\n }\n return User()\n}\n\nThis approach allows passing additional context: error code, description, or timeout. This is especially useful for logging and debugging.
\n\nResult: An Alternative to do-catch for Asynchronous Code
\n\nThe Result<Success, Failure> type is a powerful tool that comes from functional programming. It is ideal for callback-based APIs, network requests, and asynchronous operations.
enum DataError: Error {\n case corrupted\n case notFound\n}\n\nfunc loadImage(from url: URL, completion: @escaping (Result<UIImage, DataError>) -> Void) {\n DispatchQueue.global().async {\n // Simulate loading\n if arc4random_uniform(2) == 0 {\n completion(.success(UIImage()))\n } else {\n completion(.failure(.notFound))\n }\n }\n}\n\n// Usage\nloadImage(from: someURL) { result in\n switch result {\n case .success(let image):\n print("Image loaded: \\(image)")\n case .failure(let error):\n print("Error: \\(error.localizedDescription)")\n }\n}\n\nResult makes code more predictable and eliminates the need to handle optional values with nil. Additionally, you can use .map and .flatMap for transformation chains.
Rethrows: When a Function "Passes Through" Errors
\n\nThe rethrows keyword is one of the most underrated features in Swift. It allows you to write a function that does not throw errors itself but "propagates" them if the passed closure is throws.
func performOperation<T>(on array: [T], operation: (T) throws -> Bool) rethrows -> [T] {\n var result: [T] = []\n for element in array {\n if try operation(element) {\n result.append(element)\n }\n }\n return result\n}\n\n// Without errors (no try needed)\nlet numbers = [1, 2, 3, 4, 5]\nlet evenNumbers = performOperation(on: numbers) { $0.isMultiple(of: 2) }\nprint(evenNumbers) // [2, 4]\n\n// With error (try needed)\nenum ValidationError: Error { case negative }\nlet filtered = try performOperation(on: numbers) { value in\n guard value > 0 else { throw ValidationError.negative }\n return value % 2 == 0\n}\n\nrethrows makes the API flexible: the user can pass either a regular closure (without try) or a closure with throws (then try will be required). This is often used in the Swift standard library — for example, in map, filter, and for