Swift icon Swift

Result Type techniques in Swift 5

Swift 5 introduces the result type, enabling you to return a success and failure into one type without optionals! This blog post will describe the power of the result type.

Where to use the Result type?

When writing async code, you often have implementations that return an optional response and an optional error. Even Apple is using this method with URLSession:

URLSession.shared.dataTask(with: url) { (data, urlResponse, error) in
    if let error = error {
        // show error
    } else if let data = data, let urlResponse = urlResponse as? HTTPURLResponse {
        // handle response
    }
}

As you can see in the example above, we have to unwrap our values to know what is going on. The result type can improve this codebase, making the code cleaner and more readable.


What is the Result type?

The result type was proposed ([0235]) and implemented in Swift 5 and is nothing more than an enum with tuples. If we look at the implementation, the type is rather easy:

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)
}

The Success property is a generic value which can be anything, from String to custom types! The Failure property is also a generic value but the given type must conform to the Error protocol from Swift. This is enforced by the where statement at the end.

With the result type we can update the URLSession example from above. While we are at it, lets make it into an extension for reuse purposes:

extension URLSession {
    
    func dataTask(with url: URL, completionHandler: @escaping (Result<(Data, HTTPURLResponse), Error>) -> Void) -> URLSessionDataTask {
        return dataTask(with: url, completionHandler: { (data, urlResponse, error) in
            if let error = error {
                completionHandler(.failure(error))
            } else if let data = data, let urlResponse = urlResponse as? HTTPURLResponse {
                completionHandler(.success((data, urlResponse)))
            }
        })
    }
}

URLSession.shared.dataTask(with: url) { (result) in
    switch result {
    case .success(let data, let urlResponse):
        break // Handle response
    case .failure(let error):
        break // Handle error
    }
}

We have created an extension on URLSession matching the dataTask(with url:, completionHandler:) but instead of returning a tuple, it returns a result type. The Success property is now a tuple of Data and HTTPURLResponse, the Failure property is still an Error type from Swift.

When calling the new method, you now only get the result type back instead of a tuple with 3 optionals. Because the result type is an enum, we can use the switch to handle all cases. Our code example is now much more readable than before and we do not have to unwrap our values!


Checking the response of result

The example from before showed that you can switch the result response to handle the two cases. But there are many more ways to get the desired response depending on your use case. For example, you can use an if statement:

if case .success(let data, let urlResponse) = result {
    debugPrint(data)
}

This can be great if we do not care about the failure state. We can also shorten this if we do not care about the response inside the success state like this:

if case .success = result {
    debugPrint("Success")
}

This will also work for guards of course:

guard case .success(let data, let urlResponse) = result else {
    return
}
debugPrint(data)

guard case .success = result else {
    return
}
debugPrint("Success")

The Result type also offers a way to get the success case with response using get(). This is a method on result that throws the error if the case is a failure.

do {
    let (data, urlResponse) = try result.get()
    debugPrint(data)
} catch let error {
    debugPrint(error)
}

To get rid of the do catch method you could use try? and unwrap it using an if statement or a guard like this:

if let (data, urlResponse) = try? result.get() {
    debugPrint(data)
}

guard let (data, urlResponse) = try? result.get() else {
    return
}
debugPrint(data)

Map

The Result type also offers a map method, mapping either the success or failure response type to another type. For example we map our Int success to a String success:

let result: Result<Int, Error> = .success(1)

let stringResult = result.map { (value) -> String in
    return String(value)
}
// Shorter version:
let stringResult = result.map(String.init)
// stringResult will return .success("1")

Note: This does not return the value 1 as a string but a result type which success value is now of type String! There is a big difference here.

To map our failure type, we call mapError. We do have to remember the result failure implementation that the given type must conform to the Error protocol, so we create a CustomError conforming to Error:

enum CustomError: Error {
    case invalid
}

enum DataError: Error {
    case dataError1
}

let result: Result<Int, DataError> = Result.failure(.dataError1)

let customErrorResult = result.mapError { (_) -> CustomError in
    return CustomError.invalid
}
// customErrorResult will return .failure(CustomError.invalid)

FlatMap

When we want to map more advanced like only mapping odd numbers to string and returning an error for even numbers, we get a compile error because you have to map all values successfully. Lucky flatMap solve this problem. Let us implement the odd number example:

enum CustomError: Error {
    case notAnOddNumber
}

let result: Result<Int, Error> = .success(1)

let oddStringResult = result.flatMap { (value) -> Result<String, Error> in
    if value % 2 == 0 {
        return .failure(CustomError.notAnOddNumber)
    } else {
        return .success(String(value))
    }
}
// oddStringResult will return .success("1")

This Note that we return a CustomError but the oddStringResult failure type is still Error. This is because we cannot map both value and error at the same time.

FlatMapError is great when mapping a non describing error to a readable error. This is a use case that I did not find often, but that does not mean it is useless. Let us take a look at the example:

enum DataError: Error {
    case dataError1
    case dataError2
    case dataError3
}

enum CustomError: Error {
    case notAnOddNumber
    case invalidNumber
}

let result: Result<Int, DataError> = .failure(.dataError1)

let customErrorResult = result.flatMapError { (error) -> Result<Int, CustomError> in
    switch error {
    case .dataError1:
        return Result.failure(.notAnOddNumber)
    case .dataError2, .dataError3:
        return Result.failure(.invalidNumber)
    }
}
// customErrorResult will return .failure(CustomError.notAnOddNumber)

In this example we map the non describing dataError to a describing CustomError. We now know that dataError1 means that the number was not an odd number. This is a great way to make your code more describing when working with non describing errors received from frameworks.


Conclusion

The result type introduced is Swift 5 is a great way to make your code cleaner and more readable. I would highly recommend anyone to refactor your code to return result types when possible and take a look where you can implement the get(), map(), mapError(), flatMap() and flatMapError() methods. These methods are written and maintained by Apple, meaning when they made an update to increase performance on those methods, you get them for free!