Updated for Xcode 14.2
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:
data(from:)
method is async, we don’t need to use await
before it because that’s implied by async let
.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.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:
data(from:)
calls might throw, so when we read those values we need to use try
. 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:
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()
Download this as an Xcode project
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.
Tip: 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.")
Download this as an Xcode project
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.
SAVE 50% To celebrate WWDC23, all our books and bundles are half price, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.
Link copied to your pasteboard.