Teabyte

Mobile app development for iOS and Swift

Swift's Result Type

2019-07-21

At work I recently had to simplify API calls to an REST API. To do so we decided to use the Result<T,Error> type which was introduced Swift 5. With this type it is possible to have a much more cleaner and easier error handling in Swift applications. Without further ado let us dive into code to see what it is actually like to use Result.

Implementation

At first, a quick look at the beginning of the official implementation of the Result type.

/// A value that represents either a success or a failure, including an
/// associated value in each case.
public enum Result<Success, Failure> where Failure : Error {
 
    /// A success, storing a `Success` value.
    case success(Success)
 
    /// A failure, storing a `Failure` value.
    case failure(Failure)
 
    // snip
}

It is nothing more then an enumeration with two cases: success and failure. The success case will contain a generic success value whereas the failure value will conform to the Error protocol. There are additional methods defined on the Result type but for now we are totally fine with the information we have right now - Result is just an enum with two cases we can switch over.

Use it

In order to learn about the power of the Result type, imagine you want to fetch the latest SpaceX launches from the open source SpaceX API. Without using Result something like the following could be used:

func fetch() {
 
    let url =  URL(string: "https://api.spacexdata.com/v3/launches")!
 
    URLSession.shared.dataTask(with: url) { (data, response, error) in
        if let error = error {
            print("\(error)")
        }
 
        guard let response = response else {
            print("Empty response")
            return
        }
 
        guard let data = data else {
            print("Empty data")
            return
        }
        // Process your data now
    }
}

First it is necessary to check if some error occurred. Then check if the response and the data are not nil. Only after we did all of the mentioned checks we are safe and can process our data. This is a little bit cumbersome isn't it ? Let us extend URLSession to take advantage of the Result type.

Extending URLSession

extension URLSession {
    func dataTask(with url: URL, result: @escaping (Result<(URLResponse, Data), Error>) -> Void) -> URLSessionDataTask {
        return dataTask(with: url) { (data, response, error) in
            if let error = error {
                result(.failure(error))
                return
            }
 
            guard let response = response, let data = data else {
                let error = NSError(domain: "error", code: 0, userInfo: nil)
                result(.failure(error))
                return
            }
 
            result(.success((response, data)))
        }
    }
}

(Credit for this awesome extension goes to Alfian Losari, where I first got introduced to the Result type.)

Basically what we do here is to wrap our original code into a way so that all layers above URLSession can interact with the Result, type instead of the original response types. Again the check for an error is necessary but now it is possible to return the .failure(error) case. Then - again - check if data and response are not nil and if they do return another failure(error) case. If everything went fine we simple return the success(data) case, where our data is a tuple of the data and response. We did the hard work once and are now able to tremendously simplify the original fetch task.

Simplify

func fetch() {
 
    let url =  URL(string: "https://api.spacexdata.com/v3/launches")!
 
    URLSession.shared.dataTask(with: url) { (result) in
        switch result {
        case .success(let (response, data)):
            print("Do something with your data \(response, data)")
        case .failure(let error):
            print("Handle the \(error)")
        }
    }.resume()
}

With the extension in place it is possible to switch over the result and react to the different cases accordingly. Much simpler than in our original implementation where we had to check for every possible source of failure.

Conclusion

The Result types introduces a much cleaner API for handling errors. But it is not only restricted to API calls. You can use it in any situation were a method either returns a success value or can fail somehow. Your upper layers will be able to cleanly handling the error without the need of cumbersome checking beforehand.