Async Await in Swift
By: Arman Abkar
Tags: Asynchronous, Concurrency, Async Await, iOS, Swift

Async Await in Swift

In this article, we will explore the basics of async/await, the heart of the new concurrency system in Swift 5.5 Arman Abkar Sep 8·5 min read

Async Await in Swift

async/await was one of the biggest changes introduced in Swift 5.5 during WWDC 2021. Concurrency means allowing multiple pieces of code to run at the same time. This simple definition should give you an idea of how important concurrency is for the performance of the applications. With the new async methods and await statements, we can define methods running asynchronously. Besides that, async/await will simplify our complex asynchronous code and as a result, make it easier to read.

In this article, we will explore the basics of async/await. It should be noted that while async/await are not the only concurrency options, Apple’s SDKs are starting to make use of them, and there is no doubt that third-party libraries will start making use of it as well. If you have been writing callback-based concurrency, keep in mind that the implementation for async/await is completely different from anything you have seen before in Apple’s technologies and legacy codes.

What is Async?

Async (abbreviation for asynchronous) is a method attribute to tell the compiler when a piece of code is asynchronous. To mark a function as async , simply put the async keyword after the function’s closing parenthesis and before the arrow:

func downloadProfilePicture(id: Int) async -> UIImage? { }

Or:

func downloadProfilePicture(id: Int) async throws -> UIImage? { }

The method above is defined as async throwing, which means that the method would return an image if everything went well or throws an error if something went wrong.

async code can only run in concurrent contexts. It means within other async functions or when manually dispatched via Task {} .

Awaiting async

Await is the keyword to be used for calling async methods. Whenever the program finds the await keywords, it has the option of suspending the function. If the program suspends the function, await will return control. The program will then use the thread to perform other work until the suspended function is done and the statements below await will not be executed until it has finished. In other words, the program decides what’s important to execute and it will return control to you after it sees the await function has finished. So every call to an async function must be marked as await :

do {

let image = try await downloadProfilePicture ()

} catch {

print("Fetching image failed with an error \(error)")

}

In the example above, await keyword tells our program to await a result from the downloadProfilePicture method and only continue after a result arrived. This could either be an image or an error if anything went wrong while fetching the image.

By using async/await, methods are linearly executed without going back and forth as they would with closures and there would be no callback hell or completion closures anymore:

do {

let image = try await downloadProfilePicture ()

let resizedImage = try await resizeImage(image)

} catch {

print("Fetching image failed with an error \(error)")

}

As you can see, the order of execution is linear and therefore easy to follow and easy to reason. In this way, understanding asynchronous code will be easier while we’re still performing complex asynchronous tasks.

It’s important to note that it’s not guaranteed the same thread that executed the code above it, is the same one that will execute the code below it. This is crucial when working with UI. If you use await in a context that needs the main thread such as a ViewController, make sure you mark the async functions with the @MainActor attribute or add the attribute to the entire class declaration. We will discuss the Actors in another article.

Async methods in synchronous worlds

Consider the following piece of code:

func fetchImage() {

do {

try await downloadProfilePicture ()

} catch { }

}

This issue is common for the first time use of async/await. The compiler will protect us from erroneously running this, showing the following error:

‘async’ call in a function that does not support concurrency. Add ‘async’ to function ‘downloadProfilePicture()’ to make it asynchronous.

This error occurs when we try to call an asynchronous method from a synchronous calling context that does not support concurrency. The compiler suggests we mark downloadProfilePicture method as async:

func fetchImage() async {

do {

try await downloadProfilePicture ()

} catch { }

}

But this is not always possible. What if downloadProfilePicture method is in a view controller or in another place that can’t give you an asynchronous context?

To fix this problem, we can bridge this synchronous function to the asynchronous world using Task {} :

func fetchImage() {

Task.init {

do {

let image = try await downloadProfilePicture ()

} catch { }

}

}

We are explicitly creating an asynchronous context here, and it will behave as such. We can now call fetchImage method from any synchronous context without any problem.

The future of the Result type

In the examples above, We’ve seen that async methods can replace asynchronous methods that make use of closure callbacks. It seems that we won’t need the Result type anymore, as we can use try-catch statements in combination with async/await. But it’s not completely true yet.

The Result type won’t disappear anytime soon as it’s still being used in many places throughout Swift projects and many legacy codes. However, as more developers start to use async/await and their adoption rates get higher, the Result type will probably get deprecated and you can completely replace it with async/await.

Conclusion