Swift's Async Await

Helder Pinhal
Helder Pinhal
Jun 25 2021
Posted in Engineering & Technology

A first look at Swift 5.5 Async/Await support

Swift's Async Await

One of the topics on the spotlight at this year's WWDC was Swift support for the async/await mechanism. Many other programming languages like JavaScript, Dart and C# have had support for this for years. Now it's Swift's time to receive some async love.

Up until now we would use Completion Handlers to report back some values after a function had returned. The most common scenario is, perhaps, handling network requests. If one had to chain multiple of these functions with completion handlers, the code quickly becomes unreadable, hard to manage, experiencing something otherwise known as Callback Hell.

Swift 5.5 comes to change this with it's structured concurrency. Introducing the async/await principle, our code becomes far more readable and maintainable. In its essence, this allows us to run asynchronous code almost as if it was synchronous.

In this article we'll take a very first look at what Swift's async/await can provide for us. Starting by looking at a piece of code that uses completion handlers, its purpose is to fetch the characters from Star Wars from a hypothetical API, find Luke Skywalker, fetch his starships and somehow show them. Afterwards, we'll do the same using async-/await.

func fetchCharacters(_ completion: @escaping (Result<[Character], Error>) -> Void) {
    DispatchQueue.global().async {
        let characters: [Character] = [] // Perform the network request to fetch the characters.

        completion(.success(characters))
    }
}

func findLukeSkywalker(characters: [Character], _ completion: @escaping (Result<Character?, Error>) -> Void) {
    DispatchQueue.global().async {
        let luke = characters.first { $0.name == "Luke Skywalker" }

        completion(.success(luke))
    }
}

func fetchStarships(characterId: String, _ completion: @escaping (Result<[Starship], Error>) -> Void) {
    DispatchQueue.global().async {
        let starships: [Starship] = [] // Perform the network request to fetch the starships.

        completion(.success(starships))
    }
}

In order to glue those functions together, we would write something like this.

fetchCharacters { result in
    switch result {
    case let .success(characters):
        self.findLukeSkywalker(characters: characters) { result in
            switch result {
            case let .success(luke):
                guard let luke = luke else {
                    // Luke not found?
                    return
                }

                self.fetchStarships(characterId: luke.id) { result in
                    switch result {
                    case let .success(starships):
                        // Show Luke's starships.
                        break

                    case let .failure(error):
                        // Handle the error.
                        break
                    }
                }

            case let .failure(error):
                // Handle the error.
                break
            }
        }

    case let .failure(error):
        // Handle the error.
        break
    }
}

For each function with a completion handler we need to provide a closure to deal with the result. With just a few completion handler based functions, our code quickly becomes too nested.

Alternatively, we will try the same using async/await programming so that we can understand how simple it is to use it.

private func fetchCharacters() async throws -> [Character] {
    // Perform the network request to fetch the characters.
    return []
}

private func findLukeSkywalker(characters: [Character]) async throws -> Character? {
    return characters.first { $0.name == "Luke Skywalker" }
}

private func fetchStarships(characterId: String) async throws -> [Starship] {
    // Perform the network request to fetch the starships.
    return []
}

The completion handler gets dropped in favour of a normal return type, and a new async keyword is introduced. This serves to identify our functions as asynchronous.

do {
    let characters = try await fetchCharacters()
    guard let luke = try await findLukeSkywalker(characters: characters) else {
        // Luke not found?
        return
    }

    let starships = try await fetchStarships(characterId: luke.id)
    // Show Luke's starships.
} catch {
    // Handle the error.
}

Lastly, we can replace our code that executes all functions like the above. The await keyword serves to wait for the result from the asynchronous process. In a real world scenario, we would encounter far more complex scenarios involving loading, error states, etc., so by using async/await, we can drastically reduce and simplify our code.

While Swift 5.5 isn't stable yet, we're awaiting to start using it in our production libraries & apps. Until then, we'll keep experimenting with async/await in the most various scenarios.

As always, we hope you liked this article and if you have anything to add, we are available via our Support Channel.

Keep up-to-date with the latest news