Optional, throws, Result, and async/await

Back in the early days of Swift 1, we didn’t have much in the way of error handling. But we did have Optional, and it felt awesome! By making null checks explicit and enforced, bombing out of a function by returning nil suddenly felt less like a code smell and more like a language feature.

Here’s how we might write a little utility to grab Keychain data returning nil for any errors:

func keychainData(service: String) -> Data? {
  let query: NSDictionary = [
    kSecClass: kSecClassGenericPassword,
    kSecAttrService: service,
    kSecReturnData: true
  ]
  var ref: CFTypeRef? = nil

  switch SecItemCopyMatching(query, &ref) {
  case errSecSuccess:
    return ref as? Data
  default:
    return nil
  }
}

We set up a query, pass an an empty inout reference to SecItemCopyMatching and then, depending on the status code we get back, either return the reference as data or nil if there was an error.

At the call site, we can tell if something has exploded by unwrapping the optional:

if let myData = keychainData(service: "My Service") {
  do something with myData...
} else {
  fatalError("Something went wrong with... something?")
}

Getting Results

There’s a certain binary elegance to the above, but it conceals an achilles heel. At its heart, Optional is just an enum that holds either some wrapped value or nothing:

enum Optional<Wrapped> {
  case some(Wrapped)
  case none
}

This works just fine for our utility when everything goes right — we just return our value. But most operations that involve I/O can go wrong (SecItemCopyMatching, in particular, can go wrong many, many ways), and Optional ties our hands when it comes to signaling something’s gone sideways. Our only option is to return nothing.

Meanwhile, at the call site, we’re wondering what the issue is and all we’ve got to work with is this empty .none. It’s difficult to write robust software when every non-optimal condition is essentially reduced to ¯\_(ツ)_/¯. How could we improve this situation?

One way would be to add some language-level features that let functions throw errors in addition to returning values. And this is, in fact, exactly what Swift did in version 2 with its throws/throw and do/catch syntax.

But let’s stick with our Optional line of reasoning for just a moment. If the issue is that Optional can only hold a value or nil, and in the event of an error nil isn’t expressive enough, maybe we can address the issue simply by making a new Optional that holds either a value or an error?

Well, congratulations are in order: change a few names, and we see we just invented the new Result type, now available in the Swift 5 standard library!

enum Result<Success, Failure: Error> {
  case success(Success)
  case failure(Failure)
}

Result holds either a successful value or an error. And we can use it to improve our little keychain utility.

First, let’s define a custom Error type with some more descriptive cases than a simple nil:

enum KeychainError: Error {
  case notData
  case notFound(name: String)
  case ioWentBad
  ...
}

Next we change our keychainData definition to return Result<Data, Error> instead of Data?. When everything goes right we return our data as the associated value of a .success. What happens if any of SecItemCopyMatching’s many and varied disasters strike? Rather than returning nil we return one of our specific errors wrapped in a .failure:

func keychainData(service: String) -> Result<Data, Error> {
  let query: NSDictionary = [...]
  var ref: CFTypeRef? = nil

  switch SecItemCopyMatching(query, &ref) {
  case errSecSuccess:
    guard let data = ref as? Data else {
      return .failure(KeychainError.notData)
    }
    return .success(data)
  case errSecItemNotFound:
    return .failure(KeychainError.notFound(name: service))
  case errSecIO:
    return .failure(KeychainError.ioWentBad)
  ...
  }
}

Now we have a lot more information to work with at the call site! We can, if we choose, switch over the result, handling both success and each error case individually:

switch keychainData(service: "My Service") {
case .success(let data):
  do something with data...
case .failure(KeychainError.notFound(let name)):
  print(""\(name)" not found in keychain.")
case .failure(KeychainError.io):
  print("Error reading from the keychain.")
case .failure(KeychainError.notData):
  print("Keychain is broken.")
...
}

All things considered, Result seems like a pretty useful upgrade to Optional. How on earth did it take it five years to be added to the standard library?

Three’s a Crowd

Alas, Result is also cursed with an achilles heel — we just haven’t noticed it yet because, up until now, we’ve only been working with a single call to a single function. But imagine we add two more error-prone operations to our list of Result-returning utilities:

func makeAvatar(from user: Data) -> Result<UIImage, Error> {
  Return avatar made from user's initials...
  or return failure...
}

func save(image: UIImage) -> Result<Void, Error> {
  Save image and return success...
  or returns failure...
}

In our example, the first function generates an avatar from user data, and the second writes an image to disk. The implementations don’t matter so much for our purposes, just that they return a Result type.

Now, how would we write something that fetches user data from the keychain, uses it to create an avatar, saves that avatar to disk, and handles any errors that might occur along the way?

We might try something like:

switch keychainData(service: "UserData") {
case .success(let userData):

  switch makeAvatar(from: userData) {
  case .success(let avatar):

    switch save(image: avatar) {
    case .success:
      break // continue on with our program...

    case .failure(FileSystemError.readOnly):
      print("Can't write to disk.")

    ...
    }

  case .failure(AvatarError.invalidUserFormat):
    print("Unable to generate avatar from given user.")

  ...
  }

case .failure(KeychainError.notFound(let name)):
  print(""\(name)" not found in keychain.")

...
}

But whooo boy. Adding just two functions has led to an explosion of nesting, dislocated error handling, and woe.

Falling Flat

Thankfully, we can clean this up by taking advantage of the fact that, like Optional, Result implements flatMap. Specifically, flatMap on a Result will, in the case of .success, apply the given transform to the associated value and return the newly produced Result. In the case of a .failure, however, flatMap simply passes the .failure and its associated error along without modification.

Because it passes errors through in this manner, we can use flatMap to combine our operations together without checking for .failure each step of the way. This lets us minimize nesting and keep our error handling and operations distinct:

let result = keychainData(service: "UserData")
             .flatMap(makeAvatar)
             .flatMap(save)

switch result {
case .success:
  break // continue on with our program...

case .failure(KeychainError.notFound(let name)):
  print(""\(name)" not found in keychain.")
case .failure(AvatarError.invalidUserFormat):
  print("Unable to generate avatar from given user.")
case .failure(FileSystemError.readOnly):
  print("Can't write to disk.")
...
}

This is, without a doubt, an improvement. But it requires us (and anyone reading our code) to be familiar enough with .flatMap to follow its somewhat unintuitive semantics.

Compare this to the do/catch syntax from all the way back in Swift 2 that we alluded to a little earlier:

do {
  let userData = try keychainData(service: "UserData")
  let avatar = try makeAvatar(from: userData)
  try save(image: avatar)

} catch KeychainError.notFound(let name) {
  print(""\(name)" not found in keychain.")

} catch AvatarError.invalidUserFormat {
  print("Not enough memory to create avatar.")

} catch FileSystemError.readOnly {
  print("Could not save avatar to read-only media.")
} ...

The first thing that might stand out is how similar these two pieces of code are. They both have a section up top for executing our operations. And both have a section down below for matching errors and handling them.

Whereas the Result version has us piping operations through chained calls to flatMap, we write the do/catch code more or less exactly as we would if no error handling were involved. While the Result version requires we understand the internals of its enumeration and explicitly switch over it to match errors, the do/catch version lets us focus on the part we actually care about: the errors themselves.

By having language-level syntax for error handling, Swift effectively masks all the Result-related complexities it took us the first half of this post to digest: enumerations, associated values, generics, flatMap, monads… In some ways, Swift added error-handling syntax back in version 2 specifically so we wouldn’t have to deal with Result and its eccentricities.

Yet here we are, five years later, learning all about it. Why add it now?

Error’s Ups and Downs

Well, as it should happen, do/catch has this little thing we might call an achilles heel…

See, throw, like return, only works in one direction; up. We can throw an error “up” to the caller, but we can’t throw an error “down” as a parameter to another function we call.

This “up”-only behavior is typically what we want. Our keychain utility, rewritten once again with error handling, is all returns and throws because its only job is passing either our data or an error back up to the thing that called it:

func keychainData(service: String) throws -> Data {
  let query: NSDictionary = [...]
  var ref: CFTypeRef? = nil

  switch SecItemCopyMatching(query, &ref) {
  case errSecSuccess:
    guard let data = ref as? Data else {
      throw KeychainError.notData
    }
    return data
  case errSecItemNotFound:
    throw KeychainError.notFound(name: service)
  case errSecIO:
    throw KeychainError.ioWentBad
  ...
  }
}

But what if, instead of fetching user data from the keychain, we want to get it from a cloud service? Even on a fast, reliable connection, loading data over a network can take a long time compared to reading it from disk. We don’t want to block the rest of our application while we wait, of course, so we’ll make it asynchronous.

But that means we’re no longer returning anything “up”. Instead we’re calling “down” into a closure on completion:

func userData(for userID: String, completion: (Data) -> Void) {
  get data from the network
  // Then, sometime later:
  completion(myData)
}

Now network operations can fail with all sorts of different errors, but we can’t throw them “down” into completion. So the next best option is to pass any errors along as a second (optional) parameter:

func userData(for userID: String, completion: (Data?, Error?) -> Void) {
  Fetch data over the network...
  guard myError == nil else {
    completion(nil, myError)
  }
  completion(myData, nil)
}

But now the caller, in an effort to make sense of this cartesian maze of possible parameters, has to account for many impossible scenarios in addition to the ones we actually care about:

userData(for: "jemmons") { maybeData, maybeError in
  switch (maybeData, maybeError) {
  case let (data?, nil):
    do something with data...
  case (nil, URLError.timedOut?):
    print("Connection timed out.")
  case (nil, nil):
    fatalError("🤔Hmm. This should never happen.")
  case (_?, _?):
    fatalError("😱What would this even mean?")
  ...
  }
}

It’d be really helpful if, instead of this mishmash of “data or nil and error or nil” we had some succinct way to express simply “data or error”.

Stop Me If You’ve Heard This One…

Wait, data or error? That sounds familiar. What if we used a Result?

func userData(for userID: String, completion: (Result<Data, Error>) -> Void) {
  // Everything went well:
  completion(.success(myData))

  // Something went wrong:
  completion(.failure(myError))
}

And at the call site:

userData(for: "jemmons") { result in
  switch (result) {
  case (.success(let data)):
    do something with data...
  case (.failure(URLError.timedOut)):
    print("Connection timed out.")
  ...
}

Ah ha! So we see that the Result type can serve as a concrete reification of Swift’s abstract idea of “that thing that’s returned when a function is marked as throws.” And as such, we can use it to deal with asynchronous operations that require concrete types for parameters passed to their completion handlers.

So, while the shape of Result has been implied by error handling since Swift 2 (and, indeed, quite a few developers have created their own versions of it in the intervening years), it’s now officially added to the standard library in Swift 5 — primarily as a way to deal with asynchronous errors.

Which is undoubtedly better than passing the double-optional (Value?, Error?) mess we saw earlier. But didn’t we just get finished making the case that Result tended to be overly verbose, nesty, and complex when dealing with more than one error-capable call? Yes we did.

And, in fact, this is even more of an issue in the async space since flatMap expects its transform to return synchronously. So we can’t use it to compose asynchronous operations:

userData(for: "jemmons") { userResult in
  switch userResult {
  case .success(let user):
    fetchAvatar(for: user) { avatarResult in

      switch avatarResult {
      case .success(let avatar):
        cloudSave(image: avatar) { saveResult in

          switch saveResult {
          case .success:
            // All done!

          case .failure(URLError.timedOut)
            print("Operation timed out.")
          ...
        }
      }

      case .failure(AvatarError.invalidUserFormat):
        print("User not recognized.")
      ...
    }
  }

  case .failure(URLError.notConnectedToInternet):
    print("No internet detected.")
  ...
}

Awaiting the Future

In the near term, we just have to lump it. It’s better than the other alternatives native to the language, and chaining asynchronous calls isn’t as common as for synchronous calls.

But in the future, just as Swift used do/catch syntax to define away Result nesting problems in synchronous error handling, there are many proposals being considered to do the same for asynchronous errors (and asynchronous processing, generally).

The async/await proposal is one such animal. If adopted it would reduce the above to:

do {
  let user = try await userData(for: "jemmons")
  let avatar = try await fetchAvatar(for: user)
  try await cloudSave(image: avatar)

} catch AvatarError.invalidUserFormat {
  print("User not recognized.")

} catch URLError.timedOut {
  print("Operation timed out.")

} catch URLError.notConnectedToInternet {
    print("No internet detected.")
} ...

Which, holy moley! As much as I love Result, I, for one, cannot wait for it to be made completely irrelevant by our glorious async/await overlords.

Meanwhile? Let us rejoice! For we finally have a concrete Result type in the standard library to light the way through these, the middle ages of async error handling in Swift.

NSMutableHipster

Questions? Corrections? Issues and pull requests are always welcome.

This article uses Swift version 5.0. Find status information for all articles on the status page.

Next Article

Since time immemorial, iOS developers have been perplexed by a singular question: ‘How do you resize an image?’ This article endeavors to provide a clear answer to this eternal question.