Error Handling in Swift: try/catch and Beyond
In modern iOS and macOS development, application reliability is one of the key quality indicators. Proper error handling in Swift allows you not only to avoid unexpected crashes but also to make code more readable and predictable. In this article, we will cover not only the classic try/catch mechanism but also alternative approaches, including the Result type, the defer statement, and advanced patterns.
Basics: The Error Protocol and the throws Keyword
In Swift, errors are represented by types that conform to the Error protocol. Most often, enumerations (enum) are used for this purpose. For a function to be able to throw an error, the throws keyword is used in its declaration.
enum NetworkError: Error { case noInternet case serverError(code: Int) case timeout}
func fetchData(from url: String) throws -> Data { guard url.hasPrefix("https") else { throw NetworkError.serverError(code: 400) } // ... simulation of loading return Data()}Note: if a function is marked with throws, the calling code must handle the possible error using try, try?, or try!.
The do-catch Construct: The Classic Approach
The most common way to handle errors is the do-catch block. You place potentially risky code inside do, and in catch you describe what to do with each specific error.
do { let data = try fetchData(from: "http://example.com") print("Data received: \\(data)")} catch NetworkError.noInternet { print("No internet connection")} catch NetworkError.serverError(let code) { print("Server error with code: \\(code)")} catch { print("Unknown error: \\(error.localizedDescription)")}Important: the last catch block without a pattern catches all remaining errors. This is good practice to avoid missing unexpected exceptions.
try? and try!: Quick Alternatives
If you don't care about the error type and only need to know whether the function executed successfully, use try?. In case of an error, the result will be nil.
if let data = try? fetchData(from: "https://valid.com") { print("Success: \\(data)")} else { print("Error during loading")}The try! operator is a "time bomb". It suppresses error handling and causes the application to crash if an error does occur. Use it only in cases where you are 100% sure that no error will occur (for example, when loading a bundled resource).
let data = try! fetchData(from: "https://trusted-source.com")// If there is an error — the app will crashThe Result Type: A Functional Approach
Starting with Swift 5, the Result<Success, Failure> type was added to the standard library. It is an enumeration that can contain either a success value or an error. This approach is especially convenient in asynchronous code and in completion handlers.
func fetchDataResult(from url: String) -> Result<Data, NetworkError> { guard url.hasPrefix("https") else { return .failure(.serverError(code: 400)) } return .success(Data())}
// Usagelet result = fetchDataResult(from: "http://bad.com")switch result {case .success(let data): print("Data: \\(data)")case .failure(let error): print("Error: \\(error)")}The advantage of Result is that you cannot forget to handle the error, as the compiler forces you