Async await in Swift explained with code examples

Async await is part of the new structured concurrency changes that arrived in Swift 5.5 during WWDC 2021. Concurrency in Swift means allowing multiple pieces of code to run at the same time. This is a very simplified description, but it should give you an idea already how important concurrency in Swift is for the performance of your apps. With the new async methods and await statements, we can define methods performing work asynchronously.

You might have read about the Swift Concurrency Manifesto by Chris Lattner before, which was announced a few years back. Many developers in the Swift community are excited about the future to come with a structured way of defining asynchronous code. Now that it’s finally here, we can simplify our code with async-await and make our asynchronous code easier to read.

What is async?

Async stands for asynchronous and can be seen as a method attribute making it clear that a method performs asynchronous work. An example of such a method looks as follows:

func fetchImages() async throws -> [UIImage] {
    // .. perform data request
}

The fetchImages method is defined as async throwing, which means that it’s performing a failable asynchronous job. The method would return a collection of images if everything went well or throws an error if something went wrong.

How async replaces closure completion callbacks

Async methods replace the often seen closure completion callbacks. Completion callbacks were common in Swift to return from an asynchronous task, often combined with a Result type parameter. The above method would have been written as followed:

func fetchImages(completion: (Result<[UIImage], Error>) -> Void) {
    // .. perform data request
}

Defining a method using a completion closure is still possible in Swift today, but it has a few downsides that are solved by using async instead:

  • You have to make sure yourself to call the completion closure in each possible method exit. Not doing so will possibly result in an app waiting for a result endlessly.
  • Closures are harder to read. It’s not as easy to reason about the order of execution as compared to how easy it is with structured concurrency.
  • Retain cycles need to be avoided using weak references.
  • Implementors need to switch over the result to get the outcome. It’s not possible to use try catch statements from the implementation level.

These downsides are based on the closure version using the relatively new Result enum. It’s likely that a lot of projects still make use of completion callbacks without this enumeration:

func fetchImages(completion: ([UIImage]?, Error?) -> Void) {
    // .. perform data request
}

Defining a method like this makes it even harder to reason about the outcome on the caller’s side. Both value and error are optional, which requires us to perform an unwrap in any case. Unwrapping these optionals results in more code clutter which does not help to improve readability.

What is await?

Await is the keyword to be used for calling async methods. You can see them as best friends in Swift as one will never go without the other. You could basically say:

“Await is awaiting a callback from his buddy async”

Even though this sounds childish, it’s not a lie! We could take a look at an example by calling our earlier defined async throwing fetch images method:

do {
    let images = try await fetchImages()
    print("Fetched \(images.count) images.")
} catch {
    print("Fetching images failed with error \(error)")
}

It might be hard to believe, but the above code example is performing an asynchronous task. Using the await keyword, we tell our program to await a result from the fetchImages method and only continue after a result arrived. This could either be a collection of images or an error if anything went wrong while fetching the images.

What is structured concurrency?

Structured concurrency with async-await method calls makes it easier to reason about the order of execution. Methods are linearly executed without going back and forth like you would with closures.

To explain this better, we can look at how we would call the above code example before structured concurrency arrived:

// 1. Call the method
fetchImages { result in
    // 3. The asynchronous method returns
    switch result {
    case .success(let images):
        print("Fetched \(images.count) images.")
    case .failure(let error):
        print("Fetching images failed with error \(error)")
    }
}
// 2. The calling method exits

As you can see, the calling method returns before the images are fetched. Eventually, a result is received, and we go back into our flow within the completion callback. This is an unstructured order of execution and can be hard to follow. This is especially true if we would perform another asynchronous method within our completion callback which would add another closure callback:

// 1. Call the method
fetchImages { result in
    // 3. The asynchronous method returns
    switch result {
    case .success(let images):
        print("Fetched \(images.count) images.")
        
        // 4. Call the resize method
        resizeImages(images) { result in
            // 6. Resize method returns
            switch result {
            case .success(let images):
                print("Decoded \(images.count) images.")
            case .failure(let error):
                print("Decoding images failed with error \(error)")
            }
        }
        // 5. Fetch images method returns
    case .failure(let error):
        print("Fetching images failed with error \(error)")
    }
}
// 2. The calling method exits

Each closure adds another level of indentation, which makes it harder to follow the order of execution.

Rewriting the above code example by making use of async-await explains best what structured concurrency does:

do {
    // 1. Call the method
    let images = try await fetchImages()
    // 2. Fetch images method returns
    
    // 3. Call the resize method
    let resizedImages = try await resizeImages(images)
    // 4. Resize method returns
    
    print("Fetched \(images.count) images.")
} catch {
    print("Fetching images failed with error \(error)")
}
// 5. The calling method exits

The order of execution is linear and, therefore, easy to follow and easy to reason about. Understanding asynchronous code will be easier while we’re still performing sometimes complex asynchronous tasks.

Async methods call in a function that does not support concurrency

While using async-await for the first time, you might run into an error like:

Using async methods in Swift might result in an error like "'async' call in a function that does not support concurrency"
Using async methods in Swift might result in an error like “‘async’ call in a function that does not support concurrency.”

This error occurs as we try to call an asynchronous method from a synchronous calling environment that does not support concurrency. We can solve this error by either defining our fetchData method as async as well:

func fetchData() async {
    do {
        try await fetchImages()
    } catch {
        // .. handle error
    }
}

However, this would move the error to a different place. Instead, we could use the Task.init method to call the asynchronous method from a new task that does support concurrency and assign the outcome result to a property in our view model:

final class ContentViewModel: ObservableObject {
    
    @Published var images: [UIImage] = []
    
    func fetchData() {
        Task.init {
            do {
                self.images = try await fetchImages()
            } catch {
                // .. handle error
            }
        }
    }
}

Using the async method using the trailing closure, we create an environment in which we can call asynchronous methods. The fetch data method returns as soon as the async method is called, after which all asynchronous callbacks will happen within the closure.

Adopting async-await in an existing project

When adopting async-await in existing projects you want to be careful about not breaking all your code at once. When performing big refactors like these it’s good to think about maintaining old implementations for the time being so you don’t have to update all your code before you know whether your new implementation is stable enough. This is similar to deprecating methods in an SDK which is used by many different developers and projects.

Obviously, you’re not obligated to do so, but it can make it easier to try out async-await in your project. On top of that, Xcode makes it super easy to refactor your code and also provides an option to create a separate async method:

Refactor existing closure based methods into async supported methods.
Refactor existing closure-based methods into async-supported methods.

Each refactor method has its own purpose and results in different code conversions. To understand better how this works, we’ll use the following code as refactor input:

struct ImageFetcher {
    func fetchImages(completion: @escaping (Result<[UIImage], Error>) -> Void) {
        // .. perform data request
    }
}

Convert Function to Async

The first refactor option converts the fetch images method into an async variant without keeping the non-async alternative. This option will be useful if you don’t want to maintain your old implementation. The resulting code looks as follows:

struct ImageFetcher {
    func fetchImages() async throws -> [UIImage] {
        // .. perform data request
    }
}

Add Async Alternative

The add async alternative refactor option makes sure to keep the old implementation in place but does take care of adding an available attribute:

struct ImageFetcher {
    @available(*, renamed: "fetchImages()")
    func fetchImages(completion: @escaping (Result<[UIImage], Error>) -> Void) {
        Task {
            do {
                let result = try await fetchImages()
                completion(.success(result))
            } catch {
                completion(.failure(error))
            }
        }
    }


    func fetchImages() async throws -> [UIImage] {
        // .. perform data request
    }
}

The available attribute is useful to know where you need to update your code towards the new concurrency variant. Though, the default implementation Xcode provides does not come with any warning since it’s not marked as deprecated. To do so, you’ll need to adjust the available marker as follows:

@available(*, deprecated, renamed: "fetchImages()")

You can learn more about the available marker in my article How to use the #available attribute in Swift.

The benefit of using this refactor option is that it allows you to progressively adapt to the new structured concurrency changes without having to convert your complete project at once. Building in-between is valuable so that you know that your code changes work as expected. Implementations that make use of the old method will get a warning as follows:

Deprecating old implementations allows progressively updating an existing project.
Deprecating old implementations allows progressively updating an existing project.

You can progressively change your implementations throughout your project and use the provided fix button in Xcode to automatically convert your code to make use of the new implementation.

Add Async Wrapper

The final refactor method will result in the easiest conversion since it will simply make use of your existing code:

struct ImageFetcher {
    @available(*, renamed: "fetchImages()")
    func fetchImages(completion: @escaping (Result<[UIImage], Error>) -> Void) {
        // .. perform data request
    }

    func fetchImages() async throws -> [UIImage] {
        return try await withCheckedThrowingContinuation { continuation in
            fetchImages() { result in
                continuation.resume(with: result)
            }
        }
    }
}

The newly added method makes use of the withCheckedThrowingContinuation method which was introduced in Swift to convert closure-based methods without much effort. Non-throwing methods can make use of the withCheckedContinuation which works the same but does not support throwing errors.

These two methods suspend the current task until the given closure is called to trigger the continuation of the async-await method. In other words: you’ll have to make sure to call the continuation closure based on the callback of your own closure-based method. In our example, this comes down to calling the continuation with our result value returned from the original fetch images callback.

Picking the right async-await refactor method for your project

These three refactor options should be enough to convert your existing code into async alternatives. Depending on the size of your project and the amount of time you have for the refactor you might want to pick a different refactor option. Though, I would strongly advise progressively applying changes since it allows you to isolate parts that changed, making it easier to test whether your changes work as expected.

Solving the “Reference to captured parameter ‘self’ in concurrently-executing code” error

Another common error when working with async methods is the following one:

“Reference to captured parameter ‘self’ in concurrently-executing code”

This basically means that we’re trying to reference an immutable instance of self. In other words, you’re likely referencing either a property or an instance that’s immutable, for example, a structure like in the following example:

Mutating immutable properties or instances from concurrently executing code is not supported.

This error can be fixed by either making your properties mutable or by changing the struct into a reference type, like a class.

Will async await be the end of the Result enum?

We’ve seen that async methods replace asynchronous methods that make use of closure callbacks. We could ask ourselves whether this will be the end of the Result enum in Swift. In the end, we don’t really need them anymore as we can make use of try-catch statements in combination with async-await.

The Result enum won’t disappear anytime soon as it’s still being used in many places throughout Swift projects. However, I won’t be surprised to see it being deprecated once the adoption rate of async-await is getting higher. Personally, I’ve not been using the Result enum in any other place than completion callbacks. Once I’m fully using async-await, I won’t be using the enum anymore.

Continuing your journey into Swift Concurrency

The concurrency changes are more than just async-await and include many new features that you can benefit from in your code. Now that you’ve learned about the basics of async and await, it’s time to dive into other new concurrency features:

Conclusion

Async-await in Swift allows for structured concurrency, which will improve the readability of complex asynchronous code. Completion closures are no longer needed, and calling into multiple asynchronous methods after each other is a lot more readable. Several new types of errors can occur, which will be solvable by making sure async methods are called from a function supporting concurrency while not mutating any immutable references.

If you like to learn more tips on Swift, check out the Swift category page. Feel free to contact me or tweet to me on Twitter if you have any additional tips or feedback.

Thanks!