App Development

Musings on asynchronous error handling and the future of Swift

lettered tiles spelling the word error, with the first 'r' tile flipped upside-down

Swift has been open source for just about a year and a half now. In that time, there have been a multitude of proposals submitted and many substantive changes made to the Swift language. Those interested in following the discussion can check out the Swift Evolution Tracker and the Swift Weekly Breakdown to start dipping your toes in the sometimes frantic back-and-forth going on among developers.

Today I wanted to post a few thoughts on a topic that’s come up a few times in email chains, but hasn’t been officially submitted in a proposal yet. The core of the discussion revolves around the best way to handle error cases in Swift, especially around asynchronous actions. There have been a few camps forming as to the best way to go about improving Swift. The ones I see most often are adding a builtin Result type, or adding async/await functionality a la C#. Before I give my opinions, let’s dive into each of these options and see what they offer us.

Result Type

A common way for Swift developers to currently handle errors is with a Result enum that typically takes a form similar to the following:

enum Result<T> {
  case success(T)
  case failure(Error)
}

A developer can then use the result type in situations where she expects either a specific type or an error. An example may be a network call to get users:

func getUsers(api: APIProtocol, completion: ((Result<[User]>) -> Void))

In this function, the api call will call the completion closure with a list of User objects in a .success or an Error object in a .failure. It’s a convenient method for having a consistent method of error handling across an app. Consistency is a key word there, currently as it stands, the result type is up to each individual developer to implement, and I’ve personally seen countless variations on the above that do virtually the same thing [1] [2] [3]. The impetus for providing a built-in version of this Result type is that it could be applied consistently across apps and within Frameworks that could then be shared with an expected interface and easily reused. As long as it’s up to each individual developer, it’s very easy to run into conflicts where two codebases are using slightly different variations of the Result type, leading to headaches for all involved. If a user were using two different frameworks or trying to share code that used two slightly different variations on the enum, it would be tedious to translate between the two.

Async/Await

Async/await is a methodology most commonly associated with C#, where a line of code can be marked as async. This gives you the opportunity to make a closure in which you can run your asynchronous code marked with an await keyword. When the asynchronous action is done, it will automatically return the value and you can use it like any other variable. All of the asynchronous work is done in the background, while the UI remains responsive and unblocked. If you’ve ever used Promises, async/await provides similar functionality while being incredibly lightweight to code. The basic idea is to mark a particular function as being async and have it contain one or more lines marked await which will allow the method to return a task or promise that can be completed, fail, or cancelled without holding up the UI. An example usage could look similar to the following:

// actually implicitly a Task<[User]>! To the user of the return value, you treat it as a simple array of users. However it actually exists as a cancellable task until the promise is fulfilled. 
async func getUsers() -> [User] {
  // one or more lines marked with the `await` keyword
} 

// any calls to async methods are marked with `await`
let users = await getUsers()
// you can then use your `users` variable as if it was a synchronous request to get them
let names = users.map({$0.fullname})
print(names)

Async functions should be cancellable, which C# achieves by passing in a cancellation token to the asynchronous function. Something similar could work for Swift:

async func getUsers(token: CancellationToken? = nil) throws {
	// take a Cancellation token and be ready to respond to a cancellation request
}

...
let token = CancellationToken()
self.cancellationToken = token
do {
	// failed or cancelled tasks throw exceptions
let users = try await getUsers(token: token)
} catch {
	// handle cancellation error
}
…

func userTappedCancel() {
self.cancellationToken?.cancel()
}

Now on to the musings

In my opinion, a proper async/await solution seems like the cleanest, most robust solution moving forward. While I’ve made liberal use of Result enums myself, async/await mostly replaces the need for them, which makes the problem of standardizing them moot.

However, the maintainers of the Swift codebase have been pretty clear that they don’t see async/await making it into Swift 4 at the very least. Perhaps they would entertain it later on in the release cycle, but they seem reticent to put it in in the short-term. In fact, most discussion you’ll find about async/await refers to the fact that it’s too late to put it into Swift 3 and we should hold-off discussion until it is out the door. I think it’s high time we revive the discussion and find a way to bring this feature, or something similar, into Swift.

In the meantime, I’d be fine with a built-in Result type being implemented in the short-term, while we refine what we want out of an async/await solution. Discussions around making async/await have existed since 2015, during which time no solid proposals have been submitted. As long as we’re waiting, it would be nice to have a standard we could all accept and work from while waiting on a more robust async/await. In my mind, Result should be implemented during the lifecycle of Swift 4, and async/await could arrive during the lifecycle of Swift 5.

What about your thoughts? Is it worth putting in a stopgap solution in a Result type, with the associated churn to update old codebases, only to be phased out by async/await at a later date? Would you rather none of these solutions be implemented and leave it up to each developer’s preference? Is there a better solution not mentioned? Drop me a line or tweet at me and let me know what you think.

Addendum: Callbacks or Promises

A core question that was brought up during the review of this blogpost was the question of centering your asynchronous error handling around callbacks with Result or promises with async/await (or another Promise framework such as PinkyPromise (/shamelessplug). Personally, I’m fine with either solution, but I do think you should be consistent across your application. Choose to either architect around using promises or completions and stick with it. The nice thing about completion callbacks is that it is consistent with the way a great deal of UIKit is written, so the pattern of writing callbacks is familiar to any iOS developer. Promise-based solutions allow for much cleaner chaining of asynchronous operations, however. Consider the following async/await solution:

let users = await getUsers()
let details = users.map({ await getDetails(for: $0) })

In two clean lines you can express something that would normally require nested completion blocks each with their own error handling. Promise libraries such as PinkyPromise or PromiseKit allow asynchronous operation chaining in similarly simple terms.

Quickstart-Guide-to-Kotlin-Multiplatform

A Quick Start Guide to Kotlin Multiplatform

Kotlin Multiplatform, though still experimental, is a great up-and-coming solution...

Read the article