Lesson 86Error Handling

Result Type

What is Result?

The Result type is a generic enum that represents either success with a value or failure with an error. It's perfect for async operations and explicit error handling.

Result Structure

enum Result<Success, Failure: Error> {
    case success(Success)
    case failure(Failure)
}

Key Methods

get()

Convert to throwing - throws on failure

map(_:)

Transform success value

flatMap(_:)

Chain Result-returning functions

mapError(_:)

Transform error type

Result vs throws

Use Result

  • - Async completion handlers
  • - Store errors for later
  • - Multiple error paths

Use throws

  • - Synchronous code
  • - Simple propagation
  • - Cleaner syntax

Converting Between

  • Result { try throwingFunc() } - throws to Result
  • try result.get() - Result to throws
main.swift
// RESULT TYPE
// A type-safe way to handle success or failure

// RESULT DEFINITION
// enum Result<Success, Failure: Error> {
//     case success(Success)
//     case failure(Failure)
// }

// DEFINING ERRORS
enum NetworkError: Error {
    case noInternet
    case serverDown
    case invalidURL
    case timeout
}

// FUNCTION RETURNING RESULT
func fetchUser(id: Int) -> Result<String, NetworkError> {
    if id < 0 {
        return .failure(.invalidURL)
    }
    if id == 0 {
        return .failure(.serverDown)
    }
    return .success("User \(id)")
}

// HANDLING RESULT WITH SWITCH
let result = fetchUser(id: 42)

switch result {
case .success(let user):
    print("Got user: \(user)")
case .failure(let error):
    print("Error: \(error)")
}

// USING GET() METHOD
// Converts Result to throwing expression
do {
    let user = try result.get()
    print("User: \(user)")
} catch {
    print("Failed: \(error)")
}

// RESULT WITH MAP
let uppercased = result.map { $0.uppercased() }
// Result<String, NetworkError>

// RESULT WITH FLATMAP
func fetchPosts(for user: String) -> Result<[String], NetworkError> {
    return .success(["Post 1", "Post 2"])
}

let posts = result.flatMap { user in
    fetchPosts(for: user)
}

// RESULT WITH MAPERROR
enum AppError: Error {
    case network(NetworkError)
    case unknown
}

let appResult = result.mapError { networkError in
    AppError.network(networkError)
}

// CREATING RESULT FROM THROWING
func riskyOperation() throws -> Int {
    return 42
}

let fromThrowing = Result { try riskyOperation() }
// Result<Int, Error>

// RESULT IN COMPLETION HANDLERS
func fetchData(completion: @escaping (Result<Data, NetworkError>) -> Void) {
    // Simulate async work
    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
        let success = true
        if success {
            completion(.success(Data()))
        } else {
            completion(.failure(.timeout))
        }
    }
}

// Calling
fetchData { result in
    switch result {
    case .success(let data):
        print("Got \(data.count) bytes")
    case .failure(let error):
        print("Failed: \(error)")
    }
}

// PRACTICAL EXAMPLE: API Client
struct User: Codable {
    let id: Int
    let name: String
}

enum APIError: Error {
    case network(NetworkError)
    case decoding(Error)
    case invalidResponse
}

class APIClient {
    func fetchUser(id: Int) -> Result<User, APIError> {
        // Simulated
        if id > 0 {
            return .success(User(id: id, name: "John"))
        } else {
            return .failure(.invalidResponse)
        }
    }
}

// CHAINING RESULTS
let client = APIClient()
let userResult = client.fetchUser(id: 1)
    .map { user in user.name }
    .map { name in name.uppercased() }

// RESULT VS THROWS
// Use Result when:
// - Storing errors for later
// - Async completion handlers
// - Multiple error paths
// - Explicit error handling

// Use throws when:
// - Synchronous code
// - Simple error propagation
// - Cleaner syntax preferred

Try It Yourself!

Create a downloadImage(url:) function that returns Result<Data, DownloadError> and use map to convert the data to a UIImage!