Using Result in Swift 5

Published by donnywals on

As 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.


Learn Combine with my new book

Learn everything you need to know about Combine and how you can use it in your projects with my new book Practical Combine. You'll get eleven chapters, a Playground and a handful of sample projects to help you get up and running with Combine as soon as possible.

The book is available as a digital download for just $19.99!

Get Practical Combine

Receive weekly updates about my posts

Categories: Swift