Weekly Swift articles, podcasts and tips by John Sundell.

The power of Result types in Swift

Published on 17 Jun 2018

One big benefit of Swift's type system is how it lets us eliminate a lot of ambiguity when it comes to handling values and results of various operations. With features like generics and associated enum values, we can easily create types that let us leverage the compiler to make sure that we're handling values and results in a correct way.

An example of such a type is the Result type, which is being introduced into the standard library as part of Swift 5, but has also been used throughout the community for years — through custom implementations. This week, let's explore various versions of Result, and some of the cool things it lets us do when combined with some of Swift's language features.

The problem

When performing many kinds of operations, it's very common to have two distinct outcomes — success and failure. In Objective-C, those two outcomes were usually modeled by including both a value and an error, when — for example — calling a completion handler once the operation finished. However, when translated into Swift, the problem with that approach becomes quite evident — since both the value and the error have to be optionals:

func load(then handler: @escaping (Data?, Error?) -> Void) {
    ...
}

The problem is that handling the result of the above load function becomes quite tricky. Even if the error argument is nil, there's no compile-time guarantee that the data we're looking for is actually there — it might be nil as well for all we know, which would put our code in a bit of a strange state.

Separate states

Using a Result type addresses that problem by turning each result into two separate states, by using an enum containing a case for each state — one for success and one for failure:

enum Result<Value> {
    case success(Value)
    case failure(Error)
}

By making our result type generic, it can easily be reused in many different contexts, while still retaining full type safety. If we now update our load function from before to use the above result type, we can see that things become a lot more clear:

func load(then handler: @escaping (Result<Data>) -> Void) {
    ...
}

Not only does using a Result type improve the compile-time safety of our code, it also encourages us to always add proper error handling whenever we are calling an API that produces a result value — like this:

load { [weak self] result in
    switch result {
    case .success(let data):
        self?.render(data)
    case .failure(let error):
        self?.handle(error)
    }
}

We have now both made our code more clear, and we’ve also removed a source of ambiguity, leading to an API that is both more robust and nicer to use 👍.

Typed errors

In terms of type safety, we can still take things further though. In our previous iteration, the failure case of our Result enum contained an error value that could be of any type conforming to Swift's Error protocol. While that gives us a lot of flexibility, it does make it hard to know exactly what errors that might be encountered when calling a given API.

One way to solve that problem is to make the associated error value a generic type as well:

enum Result<Value, Error: Swift.Error> {
    case success(Value)
    case failure(Error)
}

That way, we are now required to specify what type of error that the API user can expect. Let's again update our load function from before, to now use the new version of our result type — with strongly typed errors:

typealias Handler = (Result<Data, LoadingError>) -> Void

func load(then handler: @escaping Handler) {
    ...
}

It can be argued that using strongly typed errors like this sort of goes against Swift's current error handling model — which doesn't include typed errors (we can only declare that a function throws, not what type of error that it might throw). However, adding that extra type information to each result does have some nice benefits — for example, it lets us specifically handle all possible errors at the call site, like this:

load { [weak self] result in
    switch result {
    case .success(let data):
        self?.render(data)
    case .failure(let error):
        // Since we now know the type of 'error', we can easily
        // switch on it to perform much better error handling
        // for each possible type of error.
        switch error {
        case .networkUnavailable:
            self?.showErrorView(withMessage: .offline)
        case .timedOut:
            self?.showErrorView(withMessage: .timedOut)
        case .invalidStatusCode(let code):
            self?.showErrorView(withMessage: .statusCode(code))
        }
    }
}

Doing error handling like we do above might seem like overkill, but "forcing" ourselves into the habit of handling errors in such a finely grained kind of way can often produce a much nicer user experience — since users will actually be informed about what went wrong, instead of just seeing a generic error screen, and we could even add appropriate actions for each error as well.

Anonymizing errors

However, given Swift's current error system, it's not always practical (or even possible) to get a strongly typed, predictable error out of every operation. Sometimes we need to use underlying APIs and systems that could produce any error, so we need some way to be able to tell the type system that our result type can contain any error as well.

Thankfully, Swift provides a very simple way of doing just that — using our good old friend NSError from Objective-C. Any Swift error can be automatically converted into an NSError, without the need for optional type casting. What's even better, is that we can even tell Swift to convert any error thrown within a do clause into an NSError, making it simple to pass it along to a completion handler:

class ImageProcessor {
    typealias Handler = (Result<UIImage, NSError>) -> Void

    func process(_ image: UIImage, then handler: @escaping Handler) {
        do {
            // Any error can be thrown here
            var image = try transformer.transform(image)
            image = try filter.apply(to: image)
            handler(.success(image))
        } catch let error as NSError {
            // When using 'as NSError', Swift will automatically
            // convert any thrown error into an NSError instance
            handler(.failure(error))
        }
    }
}

Above we'll always get an NSError in our catch block, regardless of what error that was actually thrown. While it's usually a good idea to provide a unified error API at the top level of a framework or module, doing the above can help us reduce boilerplate when it's not really important that we handle any specific errors.

Into the standard library

As part of Swift 5, the standard library is also getting its very own implementation of Result — which follows much of the same design as our last iteration (in that it supports strong typing for both values and errors). One advantage of Result being included in the standard library is that individual frameworks and apps no longer have to define their own — and more importantly, no longer have to convert between different flavors of the same kind of type.

Swift 5 also brings another interesting change that’s heavily related to Result (in fact it was implemented as part of the same Swift evolution proposal) — and that’s that the Error protocol is now self-conforming. That means that Error can now be used as a generic type that is constrained to having to conform to that same protocol, meaning that the above NSError-based technique is no longer necessary in Swift 5 — as we can simply use the Error protocol itself to anonymize errors:

class ImageProcessor {
    typealias Handler = (Result<UIImage, Error>) -> Void

    func process(_ image: UIImage, then handler: @escaping Handler) {
        do {
            var image = try transformer.transform(image)
            image = try filter.apply(to: image)
            handler(.success(image))
        } catch {
            handler(.failure(error))
        }
    }
}

The above works both for the standard library’s Result type, and for custom ones — as long as it’s constrained by the Error protocol (since other protocols are not able to be self-conforming, yet). Pretty cool! 😎

Throwing

Sometimes we don't really want to switch on a result, but rather hook it directly into Swift's do, try, catch error handling model. The good news is that since we now have a dedicated type for results, we can easily extend it to add convenience APIs. For example, Swift 5’s implementation of Result includes a get() method, that either returns the result’s value, or throws an error — which we can also implement for custom result types like this:

extension Result {
    func get() throws -> Value {
        switch self {
        case .success(let value):
            return value
        case .failure(let error):
            throw error
        }
    }
}

The above API can really become useful for tasks like writing tests, when we don't really want to add any code branches or conditionals. Here's an example in which we're testing a SearchResultsLoader by using a mocked, synchronous, network engine — and by using the above get method we can keep all assertions and verifications at the top level of our test, like this:

class SearchResultsLoaderTests: XCTestCase {
    func testLoadingSingleResult() throws {
        let engine = NetworkEngineMock.makeForSearchResults(named: ["Query"])
        let loader = SearchResultsLoader(networkEngine: engine)
        var result: Result<[SearchResult], SearchResultsLoader.Error>?

        loader.loadResults(matching: "query") {
            result = $0
        }

        let searchResults = try result?.get()
        XCTAssertEqual(searchResults?.count, 1)
        XCTAssertEqual(searchResults?.first?.name, "Query")
    }
}

To learn more about the above kind of mocking, check out “Unit testing asynchronous Swift code“.

Decoding

We can also keep adding more extensions for other common operations. For example, if our app deals a lot with decoding JSON, we could use a same type constraint to enable any Result value carrying Data to be directly decoded — by adding the following extension:

// Here we're using 'Success' as the name for the generic type
// for our result's value (rather than 'Value', like we did
// before). This is to match Swift 5's naming convention.
extension Result where Success == Data {
    func decoded<T: Decodable>(
        using decoder: JSONDecoder = .init()
    ) throws -> T {
        let data = try get()
        return try decoder.decode(T.self, from: data)
    }
}

With the above in place, we can now easily decode any loaded data, or throw if any error that was encountered — either in the loading operation itself, or while decoding:

load { [weak self] result in
    do {
        let user = try result.decoded() as User
        self?.userDidLoad(user)
    } catch {
        self?.handle(error)
    }
}

Pretty neat! 👍 The Swift 5 standard library’s implementation of Result also includes methods like map, mapError, and flatMap — which enables us to do many other kinds of transformations using inline closures and functions.

Conclusion

Using a Result type can be a great way to reduce ambiguity when dealing with values and results of asynchronous operations. By adding convenience APIs using extensions we can also reduce boilerplate and make it easier to perform common operations when working with results, all while retaining full type safety.

Whether or not to require errors to be strongly typed as well continues to be a debate within the community, and it's something I personally go back and forth a lot on too. On one hand, I like how it makes it simpler to add more thorough error handling, but on the other hand it feels a bit like fighting the system — since Swift doesn't yet support strongly typed errors as first class citizens.

What do you think? Do you use result types in your projects, and do you prefer typed or untyped errors? Let me know — along with your questions, comments or feedback — by contacting me, or on Twitter @johnsundell.

Thanks for reading! 🚀