Skip to Content
Course content

114: Result Type In Depth

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was reviewing a PR from a junior dev who had built a network layer for a weather app. He was using the old-school pattern of returning a tuple in the completion handler: completion((WeatherData?, Error?)). During a stress test, we hit a weird edge case where the server returned a 200 OK but an empty body. The completion handler fired with (nil, nil). The UI just hung there, spinning forever, because the code was checking if let data = data and if let error = error. Since both were nil, neither block executed. It was a classic "impossible state" that actually became possible.

This is exactly why the Result type exists. It turns that ambiguous "maybe this, maybe that" into a strict "either this OR that."

Escaping the Ambiguity of Optional Tuples

When you use (T?, Error?), you're telling the compiler that there are four possible outcomes: success with data, failure with an error, both, or neither. In reality, your logic only cares about two. The Result type is an enum with two cases: .success(Success) and .failure(Failure). By using it, you make the "impossible states" unrepresentable in your code.

enum APIError: Error {
    case networkFailure
    case invalidResponse
    case decodingError
}

func fetchUserBio(userId: String, completion: @escaping (Result<String, APIError>) -> Void) {
    // Simulate a network call
    let success = true 
    if success {
        completion(.success("Software Engineer and Swift enthusiast."))
    } else {
        completion(.failure(.networkFailure))
    }
}

Now, whoever calls fetchUserBio is forced to handle the result using a switch statement. They can't forget the error case, and they can't accidentally handle a state where both data and error are missing. It's a much safer contract between the function and the caller.

Handling Custom Error Types

One of the most powerful parts of Result<Success, Failure> is that the Failure type must conform to the Error protocol. While you could just use Error (the existential type), I strongly recommend using a specific enum. This allows the caller to react differently depending on why the call failed.

Think about it: you don't handle a "Wrong Password" error the same way you handle a "Server is Down" error. By defining a specific error enum for your Result, you give the UI layer the information it needs to show a helpful message instead of a generic "Something went wrong."

Transforming Results without Switching

You don't always want to switch on a result immediately. Sometimes you just want to transform the successful value and pass it along. Swift provides map and flatMap on the Result type, which lets you stay in the "Result wrapper" while modifying the inner value.

let result: Result<String, APIError> = .success("hello world")

// Use map to transform the success value
let upperCaseResult = result.map { $0.uppercased() } 
// upperCaseResult is now Result<String, APIError> containing .success("HELLO WORLD")

// Use flatMap when the transformation itself could fail
let trimmedResult = upperCaseResult.flatMap { text -> 
    text.count > 0 ? .success(text) : .failure(.invalidResponse)
}

I find that using map and flatMap keeps the code much cleaner. It allows you to chain several operations together and only "unwrap" the result at the very end when you're ready to update the UI or log the error.




📋 Practical Task

Refactoring the Legacy WeatherAPI Client

You've inherited a legacy piece of code that uses the dangerous (Data?, Error?) pattern. Your task is to refactor the WeatherService to use the Result type to ensure type safety and eliminate impossible states.

Requirements:

  • Define a WeatherError enum (conforming to Error) with cases for .noData and .serverError.
  • Change the fetchWeather function signature to use Result<WeatherData, WeatherError>.
  • Update the mock implementation to return .success or .failure.
  • Implement a caller that uses a switch statement to print the weather temperature on success or a specific error message on failure.
// Start with this legacy structure:
struct WeatherData {
    let temperature: Double
}

class WeatherService {
    func fetchWeather(completion: @escaping (WeatherData?, Error?) -> Void) {
        // Mocking a failure
        completion(nil, NSError(domain: "Weather", code: 404))
    }
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.