A more readable Result

A more readable ResultJeroen de VrindBlockedUnblockFollowFollowingFeb 24Photo by Dmitry Ratushny on UnsplashWhen making a network call we need a callback, or so called completion handler, that tells us if our call was successful and returns some data or if something has failed.

The traditional Objective-C approach of completion handlers has it’s shortcomings: it is not clear what you can get back.

Data, an error, both or neither of them.

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

}The dataTask() method from URLSession is even worse: it calls its completion handler with (Data?, URLResponse?, Error?).

So it has eight possible outcomes.

In Swift we can do better.

We have generics and enums with associated values.

We can create a Result type implemented as an enum with two cases: success and failure.

enum Result<Success, Failure: Error> { case success(Success) case failure(Failure)}They can contain any associated value as a success value, but failure must conform to Swift’s error type.

This new type has a few benefits.

First of all it makes our code a lot more readable.

Our method now becomes like this:func load(completion: @escaping (Result<Data, LoadingError>) -> Void) { .

}Or you can make it fancy with a typealias such as:typealias LoadCompletion<Data, LoadingError> = (Result<Data, LoadingError>) -> Voidand the function signature becomes like:func load(completion: @escaping LoadCompletion<Data, LoadingError> { .

}At the call-site it will look like this:load { result in switch result { case .

success(let data): // Handle loaded data case .

failure(let error): // Handle error }}The advantages are that the Result type can be reused in many different contexts, while still retaining type safety because of the use of generics.

It’s also clear what we get back: it’s either succesful data or an error.

So we have now two possible states instead of four.

The error we get back now is strongly typed.

Swift’s other error handling mechanism, throwing functions, uses errors that are unchecked and therefore can throw any type of error.

As a result you need to always add a default case.

With a Result type we can create exhaustive switch blocks.

Using throws forces often to handle errors immediately rather than store them for later processing.

Result stashes a value that we can read when needed.

Swift 5SE-0235 introduces this Result type into the standard library of Swift 5.

If you might already use a Result type in your project like above or from some library, then that type will be used instead of Swift’s own Result type, so you can upgrade without breaking your code.

The Result type in Swift 5 has some nice additional features.

It comes with a get() method that returns the successful value if it exists or throws an error otherwise.

This makes it possible to convert Result into a throwing call:load { result in if let data = try?.result.

get() { print(data) }}You can use regular if statements to read the cases of an enum like this:if case .

success(let data) = result { print "Data: (data)"}Result has an initializer that accepts a throwing closure, for example init(contentsOf url: URL) throws.

If the closure returns successfully a value, it is placed in the success case of Result.

If it throws an error, it’s placed in the error case:let result = Result { try String(contentsOfFile: someFile) }Instead of using a specific error enum, you can also use the general Error protocol in Swift’s 5 Result type like: Result<Data, Error>.

Although you lose the safety of typed errors, you can now throw a variety of different error enums.

Furthermore it’s good to know you can use Result also with methods like map(), flatMap(), mapError() and flatMapError().

The map() method, for example, transforms the success value into a different kind of value that you specify in the closure.

If there’s a failure instead it just uses that and ignores the transformation.


. More details

Leave a Reply