Using Result in Swift 5
Published on: March 2, 2020As soon as Swift was introduced, people were adding their own extensions and patterns to the language. One of the more common patterns was the usage of a Result
object. This object took on a shape similar to Swift's Optional
, and it was used to express a return type that could either be a success or a failure. It took some time, but in Swift 5.0 the core team finally decided that it was time to adopt this common pattern that was already used in many applications and to make it a part of the Swift standard library. By doing this, the Swift team formalized what Result
looks like, and how it works.
In today's post, my goal is to show you how and when Result
is useful, and how you can use it in your own code. By the end of this post, you should be able to refactor your own code to use the Result
object, and you should be able to understand how code that returns a Result
should be called.
Writing code that uses Swift's Result type
Last week, I wrote about Swift's error throwing capabilities. In that post, you saw how code that throws errors must be called with a special syntax, and that you're forced to handle errors. Code that returns a Result
is both very different yet similar to code that throws at the same time.
It's different in the sense that you can call code that returns a Result
without special syntax. They're similar in the sense that it's hard to ignore errors coming from a Result
. Another major difference is how each is used in an asynchronous environment.
If your code runs asynchronously, you can't just throw an error and force the initiator of the asynchronous work to handle this error. Consider the following non-functional example:
func loadData(from url: URL, completion: (Data?) -> Void) throws {
URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
throw error
}
if let data = data {
completion(data)
}
}.resume()
}
Other than the fact that Swift won't compile this code because the closure that's passed as the data task's completion handler isn't marked as throwing, this code doesn't make a ton of sense. Let's examine what the call site for this code would potentially look like:
do {
try loadData(from: aURL) { data in
print("fetched data")
}
print("This will be executed before any data is fetched from the network.")
} catch {
print(error)
}
This isn't useful at all. The idea of using try
and throwing errors is that the code in the do
block immediately moves to the catch
when an error occurs. Not that all code in the do
is executed before any errors are thrown, because the data task in loadData(from:completion:)
runs asynchronously. In reality, the error that's potentially thrown in the data task's completion handler never actually makes it out of the completion handler's scope. So to summarize this paragraph, it's safe to say that errors thrown in an asynchronous environment never make it to the call-site.
Because of this, Swift's error throwing doesn't lend itself very well for asynchronous work. Luckily, that's exactly where Swift's Result
type shines.
A Result
in Swift is an enum
with a success
and failure
case. Each has an associated value that will hold either the success value or an error if the result is a failure
. Let's look at Result
's definition real quick:
/// A value that represents either a success or a failure, including an
/// associated value in each case.
@frozen
public enum Result<Success, Failure: Error> {
/// A success, storing a `Success` value.
case success(Success)
/// A failure, storing a `Failure` value.
case failure(Failure)
}
The real definition of Result
is much longer because several methods are implemented on this type, but this is the most important part for now.
Let's refactor that data task from before using Result
so it compiles and can be used:
func loadData(from url: URL, completion: (Result<Data?, URLError>) -> Void) throws {
URLSession.shared.dataTask(with: url) { data, response, error in
if let urlError = error as? URLError {
completion(.failure(urlError))
}
if let data = data {
completion(.success(data))
}
}.resume()
}
loadData(from: aURL) { result in
// we can use the result here
}
Great, we can now communicate errors in a clean manner to callers of loadData(from:completion:)
. Because Result
is an enum, Result
objects are created using dot syntax. The full syntax here would be Result.failure(urlError)
and Result.success(data)
. Because Swift knows that you're calling completion
with a Result
, you can omit the Result
enum.
Because the completion
closure in this code takes a single Result
argument, we can express the result of our work with a single object. This is convenient because this means that we don't have to check for both failure and success. And we also make it absolutely clear that a failed operation can't also have a success value. The completion closure passed to URLSession.shared.dataTask(with:completionHandler:)
is far more ambiguous. Notice how the closure takes three arguments. One Data?
, one URLResponse?
and an Error?
. This means that in theory, all arguments can be nil
, and all arguments could be non-nil
. In practice, we won't have any data, and no response if we have an error. If we have a response, we should also have data and no error. This can be confusing to users of this code and can be made cleaner with a Result
.
If the data task completion handler would take a single argument of type Result<(Data, URLResponse), Error>
. It would be very clear what the possible outcomes of a data task are. If we have an error, we don't have data and we don't have a response. If the task completes successfully, the completion handler would receive a result that's guaranteed to have data and a response. It's also guaranteed to not have any errors.
Let's look at one more example expressing the outcome of asynchronous code using Result
before I explain how you can use code that provides results with the Result
type:
enum ConversionFailure: Error {
case invalidData
}
func convertToImage(_ data: Data, completionHandler: @escaping (Result<UIImage, ConversionFailure>) -> Void) {
DispatchQueue.global(qos: .userInitiated).async {
if let image = UIImage(data: data) {
completionHandler(.success(image))
} else {
completionHandler(.failure(ConversionFailure.invalidData))
}
}
}
In this code, I've defined a completion handler that takes Result<UIImage, ConversionFailure>
as its single argument. Note that the ConversionFailure
enum conforms to Error
. All failure cases for Result
must conform to this protocol. This code is fairly straightforward. The function I defined takes data and a completion handler. Because converting data to an image might take some time, this work is done off the main thread using DispatchQueue.global(qos: .userInitiated).async
. If the data is converted to an image successfully, the completion handler is called with .success(image)
to provide the caller with a successful result that wraps the converted image. If the conversion fails, the completion handler is called with .failure(ConversionFailure.invalidData)
to inform the caller about the failed image conversion.
Let's see how you could use the convertToImage(_:completionHandler:)
function, and how you can extract the success or failure values from a Result
.
Calling code that uses Result
Similar to how you need to do a little bit of work to extract a value from an optional, you need to do a little bit of work to extract the success or failure values from a Result
. I'll start with showing the simple, verbose way of extracting success and failure from a Result
:
let invalidData = "invalid!".data(using: .utf8)!
convertToImage(invalidData) { result in
switch result {
case .success(let image):
print("we have an image!")
case .failure(let error):
print("we have an error! \(error)")
}
}
This example uses a switch
and Swift's powerful pattern maching capabilities to check whether result
is .success(let image)
or .failure(let error)
. Another way of dealing with a Result
is using its built in get
method:
let invalidData = "invalid!".data(using: .utf8)!
convertToImage(invalidData) { result in
do {
let image = try result.get()
print("we have an image!")
} catch {
print("we have an error \(error)")
}
}
The get
method that's defined of Result
is a throwing method. If the result is successful, get()
will not throw an error and it simply returns the associated success value. In this case that's an image. If the result isn't success
, get()
throws an error. The error that's thrown by get()
is the associated value of the Result
object's .failure
case.
Both ways of extracting a value from a Result
object have a roughly equal amount of code, but if you're not interested in handling failures, the get()
method can be a lot cleaner:
convertToImage(invalidData) { result in
guard let image = try? result.get() else {
return
}
print("we have an image")
}
If you're not sure what the try
keyword is, make sure to check out last week's post where I explain Swift's error throwing capabilities.
In addition to extracting results from Result
, you can also map over it to transform a result's success value:
convertToImage(invalidData) { result in
let newResult = result.map { uiImage in
return uiImage.cgImage
}
}
When you use map
on a Result
, it creates a new Result
with a different success
type. In this case, success
is changed from UIImage
to CGImage
. It's also possible to change a Result
's error:
struct WrappedError: Error {
let cause: Error
}
convertToImage(invalidData) { result in
let newResult = result.mapError { conversionFailure in
return WrappedError(cause: conversionFailure)
}
}
This example changes the result's error from ConversionError
to WrappedError
using mapError(_:)
.
There are several other methods available on Result
, but I think this should set you up for the most common usages of Result
. That said, I highly recommend looking at the documentation for Result
to see what else you can do with it.
Wrapping a throwing function call in a Result type
After I posted my article on working with throwing functions last week, it was pointed out to me by Matt Massicotte that there is a cool way to initialize a Result
with a throwing function call with the Result(catching:)
initializer of Result
. Let's look at an example of how this can be used in a network call:
func loadData(from url: URL, _ completion: @escaping (Result<MyModel, Error>) -> Void) {
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else {
if let error = error {
completion(.failure(error))
return
}
fatalError("Data and error should never both be nil")
}
let decoder = JSONDecoder()
let result = Result(catching: {
try decoder.decode(MyModel.self, from: data)
})
completion(result)
}
}
The Result(catching:)
initializer takes a closure. Any errors that are thrown within that closure are caught and used to create a Result.failure
. If no errors are thrown in the closure, the returned object is used to create a Result.success
.
In Summary
In this week's post, you learned how you can write asynchronous code that exposes its result through a single, convenient type called Result
. You saw how using Result
in a completion handler is clearer and nicer than a completion handler that takes several optional arguments to express error and success values, and you saw how you can invoke your completion handlers with Result
types. You also learned that Result
types have two generic associated types. One for the failure case, and one for the success case.
You also saw how you can call out to code that exposes its result through a Result
type, and you learned how you can extract and transform both the success and the failure cases of a Result
.
If you have any feedback or questions for me about this post or any of my other posts, don't hesitate to send me a Tweet.