Logo for tanaschita.com

Quick guide on async/await in Swift

Learn how use async/await for asynchronous tasks.

01 Jul 2021 · 7 min read

During the WWDC21, Apple introduced the new async/await syntax in Swift version 5.5, allowing us to write asynchronous code in a shorter and safer way.

Let's directly jump into an example and look at a scenario where we are creating a view model that depends on two API requests.

The callback solution

To point out the advantages of the async/await solution, we will at first take a look at the solution with completion callbacks that are commonly used for asynchronous tasks.

func loadUserProfileViewModel(username: String, password: String,
completion: ((Result<UserProfileViewModel, ServerError>) -> Void)) {
api.loginUser(username: username, password: password) { result in
switch result {
case .success(let user):
api.loadMessages(user) { result in
switch result {
case .success(let messages):
let viewModel = UserProfileViewModel(user, messages)
completion(.success(viewModel))
case .failure(let error):
completion(.failure(error))
}
}
case .failure(let error):
completion(.failure(error))
}
}
}

As we can see above, nested completion callbacks produce a lot of boilerplate code. We need to evaluate the result of every call before we can continue to the next. And if we forget to call a completion, the compiler won't warn us.

The async/await solution

The same task can be solved with async/await like this:

func loadUserProfileViewModel(username: String, password: String) async throws -> UserProfileViewModel {
let user = try await api.loginUser(username: username, password: password)
let messages = try await api.loadMessages(user)
return UserProfileViewModel(user, messages)
}

With the async/await approach, we got rid of all indentations and reduced our function from 16 to only 3 lines of code!

The async/await approach has also given our function more safety, as it is now guaranteed to return a result, whereas a completion handler is not guaranteed to be called.

Just like with completion handlers, the calling thread is not blocked while waiting for the results.

The async/await syntax

Let's take a closer look on the syntax for async/await.

To mark a function as asynchronous we use the async keyword which allows the function to suspend.

func someComplexCalculations() async -> Int {
}

If the function additionally can fail with an error, we can add the throw keyword.

func someComplexCalculations() async throw -> Int {
}

Now, if we want to call that function, we have to use the await keyword which marks where the function may suspend.

let result = await someComplexCalculations()

Or with a throwing function:

do {
let result = await someComplexCalculations()
} catch let error {
print(error)
}

Once the awaited async call completes, the execution resumes.

Async/await properties and initializers

Not only functions, but also read-only properties can be asynchronous.

struct User {
var profileImage: UIImage? {
get async {
let (data, response) = try await URLSession.shared.data(from: url)
return UIImage(data: data)
}
}
}

Initializers can also be marked as asynchronous.

init() async {
}

Using async functions provided by the SDK

A lot of built-in SDK functions have been adapted to be async. For example URLSession comes with async functions to load or upload data.

let (data, response) = try await URLSession.shared.data(from: imageURL)
let image = UIImage(data: data)

In the example above, we are using the data(from url: URL) async throws -> (Data, URLResponse) method to load an image from a given image url.

Writing async functions with continuations

To write our own async/await functions, Swift provides so called continuations. Continuation functions give us control over suspending and resuming the function.

func fibonacciSequence(count: Int) async -> [Int] {
return await withCheckedContinuation({ continuation in
fibonacciSequence(count: count) { result in
continuation.resume(returning: result)
}
})
}

In the above example, we use an existing completion handler function to wrap it in a async function.

As soon as we call withCheckedContinuation(function:_:), the function suspends.

Continuations must be resumed exactly once on every path. The compiler produces an error, if we forget to resume or call resume multiple times.

If the function needs to throw an error, we can use withCheckedThrowingContinuation(function:_:) instead.

Async sequences

Swift also provides the ability to use async/await in for loops by using AsyncSequence.

for await line in url.lines {
}

The lines property of URL is an async sequence that provides the ability to read the contents of a URL line-by-line. Since it conforms to the AsyncSequence protocol, we are able to for loop over it with the await syntax.

Further reading

Calling an asynchronous function with await runs only one piece of code at a time. The caller waits for it to finish before running the next line of code.

Sometimes, we want to be able to start multiple async functions in parallel because they don't depend on each other. This is where tasks come in. Read this guide on how to call async/await functions concurrently in Swift to learn more.

Related tags

Written by