How to call an async function using async let
How to call an async function using async let êŽë š
Updated for Xcode 15
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, _) = URLSession.shared.data(from: URL(string: "https://hws.dev/user-24601.json")!)
async let (messageData, _) = URLSession.shared.data(from: URL(string: "https://hws.dev/user-messages.json")!)
// 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 useawait
before it because thatâs implied byasync let
. - The
data(from:)
method is also throwing, but we donât need to usetry
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 usetry
. - Both our
data(from:)
calls are running concurrently while our mainloadData()
function continues to execute, so we need to read their values usingawait
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:
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
}
func loadData() async {
async let (userData, _) = URLSession.shared.data(from: URL(string: "https://hws.dev/user-24601.json")!)
async let (messageData, _) = URLSession.shared.data(from: URL(string: "https://hws.dev/user-messages.json")!)
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 \(user.name) has \(messages.count) message(s).")
} catch {
print("Sorry, there was a network problem.")
}
}
await loadData()
The Swift compiler will automatically track which async let
constants could throw errors and will enforce the use of try
when reading their value. It doesnât matter which form of try
you use, so you can use try
, try?
or try!
as appropriate.
Tips
If you never try to read the value of a throwing async let
call â i.e., if youâve started the work but donât care what it returns â then you donât need to use try
at all, which in turn means the function running the async let
code might not need to handle errors at all.
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:
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
}
func fetchFavorites(for user: User) async -> [Int] {
print("Fetching favorites for \(user.name)âŠ")
do {
async let (favorites, _) = URLSession.shared.data(from: URL(string: "https://hws.dev/user-favorites.json")!)
return try await JSONDecoder().decode([Int].self, from: favorites)
} catch {
return []
}
}
let user = User(id: UUID(), name: "Taylor Swift", age: 26)
async let favorites = fetchFavorites(for: user)
await print("Found \(favorites.count) favorites.")
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 change the user:
var user = User(id: UUID(), name: "Taylor Swift", age: 26)
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.