Using Async/Await in Swift development

Apple has finally accepted the proposal for the async paradigm in Swift. While it is currently only in the development trunk of the language, it will soon be available in the final release. Let’s have a look at what we can expect.

Marcel Kulina
4 min readMar 9, 2021

Sidenote: This article is mainly for showcasing async code in Swift. In order to use async programming now, you have to download the beta toolchain and make XCode use that version, instead of the official one. I do not cover setup in this article as the language feature will soon be released in the official branch.

The old way: Closures

Before async code existed in Swift, we could easily end up with a closure hell, looking like this:

// Controller
viewmodel.requestData { data in (...) }
// ViewModel
func requestData(completion: @escaping () -> Void) { service.requestData { response in
response.getData { data in
completion(data)
}
}
}

While this is fine in general, the level of nested calls can get very deep, and it becomes hard to debug issues.

Another problem is concurrency. While a function might have returned already, the closure did not finish yet, making the execution of our code more random than predictable.

The modern way: Async and Await

Async programming enables us to stay in control of our execution flow. Callbacks don’t fire after at any time, we don’t need to call multiple callbacks and make sure of the timing. Async helps us maintain a clear flow.

The pattern isn’t new. In fact languages like C# feature async programming for years now, making it a very matured paradigm.

A quick example. Assuming, we have a method called getUserData(userId), and we want to compare two users.

Written in almost plain English, async pseudocode could look like this:

User1 = Try Wait for getUserData(id: 1)
User2 = Try Wait for getUserData(id: 2)
Is User1 Name = User2 Name ?

We already see an important keyword here, wait, or in actual code, await.

But let’s break this down into pieces.

First, we create a User1 object and store our getUserData(1) result in it, only if the call succeeds. The method does wait for getUserData() to finish execution. We do the same for User2 afterward. After that, we return the check of equality.

Nothing too special, so what is the big difference here?

Safety: We can assume that User1 and User2 will be populated when the check occurs. Otherwise, an error will be thrown.

Concurrency: User1 will always be populated before User2, unlike closures, where the Closure for User2 could fire first.

Expressiveness: Instead of passing a closure, we directly indicate that we want our objects to be the result of the called method. We don’t need to ensure that our closures populate our objects properly. Additionally, we have error handling directly via try. There is no need to check for null or anything else in our closures.

Implementation

Writing applications in an asynchronous way is not as different as one would think. Besides some differences in design, our code mostly stays the same. Additionally, we get all the benefits mentioned above with visually no drawbacks.

General example

Having said that, here is one way in which our pseudocode could be implemented with the old Closure approach:

getUserData(id: 1) { user1 in 
getUserData(id: 2) { user2 in
getUserData(id: 3) {user3 in
return user1.name == user2.name && user2.name == user3.name
}
}
}
func getUserData(id: Int, completion: @escaping (User) -> Void) {
let user = ...
completion(user)
}

The mentioned downsides already apply to this simple example. To make sure that user1 and user2 exist at the same time, we need to have nested closures, leading to unreadable, ugly, code.

Now let’s have a look at the same code written the async way:

let user1 = await getUserData(id: 1)
let user2 = await getUserData(id: 2)
let user3 = await getUserData(id: 3)
return user1.name == user2.name && user2.name == user3.namefunc getUserData() async throws -> Void {
let user = try await ...
return user
}

Error handling

Ensuring correctness becomes a lot easier with async functions as well. Instead of using guards per closure, we can make use of async try. This makes the big gains through async code a lot more visible:

getUserData(id: 1) { user1 in 
guard let usr1 = user1 else { fatalError() }
getUserData(id: 2) { user2 in
guard let usr2 = user2 else { fatalError() }
getUserData(id: 3) {user3 in
guard let usr3 = user1 else { fatalError() }
return usr1.name == usr2.name && usr2.name == usr3.name
}
}
}

Again, the same async code would look like this:

let user1 = try await getUserData(id: 1)
let user2 = try await getUserData(id: 2)
let user3 = try await getUserData(id: 3)
return user1.name == user2.name && user2.name == user3.name

I sight into the future

Async code will change how we develop apps in Swift. While the first proposal that made it into the language is far from being as powerful as C#, Swift is heading in the right direction. Overall, our code will become more readable and robust, without much additional effort for developers. I am excited for async code making it into apps soon.

--

--