FREE TRIAL: Accelerate your app development career with Hacking with Swift+! >>

How to call an async function using async let

Paul Hudson    @twostraws   

Sometimes you want to run several async operations at the same time then wait for their results to come back, and the easiest way to do that is with async let. This lets you start several async functions, all of which begin running immediately – it’s much more efficient than running them sequentially.

A common example of where this is useful is when you have to make two or more network requests, none of which relate to each other. That is, if you need to get Thing X and Thing Y from a server, but you don’t need to wait for X to return before you start fetching Y.

To demonstrate this, we could define a couple of structs to store data – one to store a user’s account data, and one to store all the messages in their inbox:

struct User: Decodable {
    let id: UUID
    let name: String
    let age: Int

struct Message: Decodable, Identifiable {
    let id: Int
    let from: String
    let message: String

These two things can be fetched independently of each other, so rather than fetching the user’s account details then fetching their message inbox we want to get them both together.

In this instance, rather than using a regular await call a better choice is async let, like this:

func loadData() async {
    async let (userData, _) = URL(string: "")!)

    async let (messageData, _) = URL(string: "")!)

    // more code to come

That’s only a small amount of code, but there are three things I want to highlight in there:

  • Even though the data(from:) method is async, we don’t need to use await before it because that’s implied by async let.
  • The data(from:) method is also throwing, but we don’t need to use try to execute it because that gets pushed back to when we actually want to read its return value.
  • Both those network calls start immediately, but might complete in any order.

Okay, so now we have two network requests in flight. The next step is to wait for them to complete, decode their returned data into structs, and use that somehow.

There are two things you need to remember:

  • Both our data(from:) calls might throw, so when we read those values we need to use try.
  • Both our data(from:) calls are running concurrently while our main loadData() function continues to execute, so we need to read their values using await in case they aren’t ready yet.

So, we could complete our function by using try await for each of our network requests in turn, then print out the result:

do {
    let decoder = JSONDecoder()
    let user = try await decoder.decode(User.self, from: userData)
    let messages = try await decoder.decode([Message].self, from: messageData)
    print("User \( has \(messages.count) message(s).")
} catch {
    print("Sorry, there was a network problem.")

The Swift compiler will automatically track which async let constants could throw errors and will enforce the use of try when reading their value.

Tip: It doesn’t matter which form of try you use, so you can use try, try? or try! as appropriate.

Although both our network requests are happening at the same time, we still need to wait for them to complete in some sort of order. So, if you wanted to update your user interface as soon as either user or messages arrived back async let isn’t going to help by itself – you should look at the dedicated Task type instead.

One complexity with async let is that it captures any values it uses, which means you might accidentally try to write code that isn’t safe. Swift helps here by taking some steps to enforce that you aren’t trying to modify data unsafely.

As an example, if we wanted to fetch the favorites for a user, we might have a function such as this one:

func fetchFavorites(for user: User) async -> [Int] {
    print("Fetching favorites for \(…")

    do {
        async let (favorites, _) = URL(string: "")!)
        return try await JSONDecoder().decode([Int].self, from: favorites)
    } catch {
        return []

That function accepts a User parameter so it can print a status message. But what happens if our User was created as a variable and captured by async let? You can see this for yourself if you try the following code:

var user = User(id: UUID(), name: "Taylor Swift", age: 26)
async let favorites = fetchFavorites(for: user)
await print("Found \(favorites.count) favorites.")

Even though it’s a struct, the user variable will be captured rather than copied and so Swift will throw up the build error “Reference to captured var 'user' in concurrently-executing code.”

To fix this we need to make it clear the struct cannot change by surprise, even when captured, by making it a constant rather than a variable:

let user = User(id: UUID(), name: "Taylor Swift", age: 26)
Hacking with Swift is sponsored by Essential Developer

SPONSORED Join a FREE crash course for iOS devs who want to become complete senior developers — from October 18th to 24th. Learn how to apply iOS app architecture patterns through a series of lectures and practical coding sessions.

Learn more

Sponsor Hacking with Swift and reach the world's largest Swift community!

Buy Pro Swift Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift (Vapor Edition) Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Server-Side Swift (Kitura Edition) Buy Beyond Code

Was this page useful? Let us know!

Unknown user

You are not logged in

Log in or create account

Link copied to your pasteboard.