Error Handling in Swift: 3 Practical Examples

Explore three practical examples of error handling in Swift to improve your programming skills.
By Jamie

Error Handling in Swift

Error handling is a crucial aspect of programming in Swift, allowing developers to manage errors gracefully and maintain robust applications. Swift uses a combination of do, try, and catch keywords to handle errors. This article presents three diverse examples of error handling in Swift to help you understand its implementation in real-world scenarios.

Example 1: Reading from a File

When working with file systems, errors can arise from missing files or incorrect paths. This example demonstrates how to handle such errors when attempting to read content from a file.

In this use case, we want to read a text file and handle potential errors related to file access.

import Foundation

enum FileError: Error {
    case fileNotFound(String)
    case unreadable(String)
}

func readFile(at path: String) throws -> String {
    let fileURL = URL(fileURLWithPath: path)
    guard FileManager.default.fileExists(atPath: path) else {
        throw FileError.fileNotFound(path)
    }
    do {
        let content = try String(contentsOf: fileURL, encoding: .utf8)
        return content
    } catch {
        throw FileError.unreadable(path)
    }
}

let filePath = "path/to/file.txt"

do {
    let content = try readFile(at: filePath)
    print(content)
} catch FileError.fileNotFound(let path) {
    print("Error: File not found at \(path)")
} catch FileError.unreadable(let path) {
    print("Error: Unable to read file at \(path)")
} catch {
    print("An unexpected error occurred: \(error)")
}

In this example, we define a custom error type FileError to categorize potential errors. The readFile function checks if the file exists and attempts to read it, throwing appropriate errors if issues are encountered.

Example 2: Network Request Handling

When making network requests, various issues such as connectivity problems or invalid responses can occur. This example illustrates how to handle errors during a network call using URLSession.

In this case, we want to fetch data from a web API and correctly handle possible network errors.

import Foundation

enum NetworkError: Error {
    case badURL(String)
    case requestFailed(Error)
    case invalidResponse
}

func fetchData(from urlString: String, completion: @escaping (Result<Data, NetworkError>) -> Void) {
    guard let url = URL(string: urlString) else {
        completion(.failure(.badURL(urlString)))
        return
    }
    let task = URLSession.shared.dataTask(with: url) { data, response, error in
        if let error = error {
            completion(.failure(.requestFailed(error)))
            return
        }
        guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
            completion(.failure(.invalidResponse))
            return
        }
        if let data = data {
            completion(.success(data))
        }
    }
    task.resume()
}

let url = "https://api.example.com/data"

fetchData(from: url) { result in
    switch result {
    case .success(let data):
        print("Data received: \(data)")
    case .failure(let error):
        switch error {
        case .badURL(let urlString):
            print("Error: Bad URL \(urlString)")
        case .requestFailed(let error):
            print("Error: Request failed with error: \(error)")
        case .invalidResponse:
            print("Error: Received invalid response")
        }
    }
}

This example uses a completion handler to return the outcome of the network request, encapsulated in a Result type. The NetworkError enum captures various error scenarios, providing clear feedback on what went wrong during the request.

Example 3: Parsing JSON Data

Parsing JSON data can often lead to errors, especially when the data structure does not match expectations. This example shows how to handle errors when decoding JSON into Swift structs.

In this scenario, we want to decode a JSON response from a web API into a Swift struct and handle potential decoding errors.

import Foundation

struct User: Codable {
    let id: Int
    let name: String
}

enum DecodingError: Error {
    case invalidData
}

func decodeUser(from jsonData: Data) throws -> User {
    let decoder = JSONDecoder()
    do {
        let user = try decoder.decode(User.self, from: jsonData)
        return user
    } catch {
        throw DecodingError.invalidData
    }
}

let jsonString = "{\"id\": 1, \"name\": \"John Doe\"}"
if let jsonData = jsonString.data(using: .utf8) {
    do {
        let user = try decodeUser(from: jsonData)
        print("User: \(user.name)")
    } catch DecodingError.invalidData {
        print("Error: Failed to decode user data.")
    } catch {
        print("An unexpected error occurred: \(error)")
    }
}

Here, we define a User struct that conforms to the Codable protocol for easy JSON decoding. The decodeUser function attempts to decode the JSON data and throws an error if it fails, allowing for precise error handling during the decoding process.

By understanding and implementing these examples of error handling in Swift, you can create applications that are more resilient and user-friendly, effectively managing errors and improving overall user experience.