Chapter 5 — async let: concurrent children
The problem: sequential when you meant concurrent
// SLOW — three independent loads, run one after another.
func loadDashboard() async throws -> Dashboard {
let user = try await client.fetch(User.self, from: userURL) // wait 300ms
let weather = try await client.fetch(Weather.self, from: weatherURL) // THEN wait 250ms
let news = try await client.fetch([Article].self, from: newsURL) // THEN wait 400ms
return Dashboard(user: user, weather: weather, news: news) // total ≈ 950ms
}
flowchart TB
subgraph SEQ__Sequential_await____sum___950ms___ [\"Sequential await (≈ sum = 950ms)\"]
U1[user 300] --> W1[weather 250] --> N1[news 400]
end
subgraph CON__async_let____max___400ms___ [\"async let (≈ max = 400ms)\"]
U2[user 300]
W2[weather 250]
N2[news 400]
end
async let starts work immediately
// FAST — three concurrent child tasks; total ≈ the slowest one.
func loadDashboard() async throws -> Dashboard {
async let user = client.fetch(User.self, from: userURL) // starts NOW
async let weather = client.fetch(Weather.self, from: weatherURL) // starts NOW
async let news = client.fetch([Article].self, from: newsURL) // starts NOW
// All three are already running concurrently. Now collect them:
return try await Dashboard(user: user, weather: weather, news: news)
}
The key distinction: a plain
let x = await f()waits forfbefore continuing. Anasync let x = f()startsfand continues immediately; the waiting happens later, at the point youawait x.
async let is structured
flowchart TB
SCOPE["loadDashboard scope"]
SCOPE --> C1["child: user"]
SCOPE --> C2["child: weather"]
SCOPE --> C3["child: news"]
C1 & C2 & C3 --> EXIT["scope cannot exit until
all children finish or are cancelled"]
all children finish or are cancelled"]
- The scope can't exit while children are running. You cannot return from (or fall off the end of)
- Errors propagate like normal
throws. If any child throws (a network failure), thetry await - Cancellation flows down. If the task running
loadDashboardis cancelled, all threeasync let
You must await (or the compiler complains)
func partialUse() async throws -> User {
async let user = client.fetch(User.self, from: userURL)
async let weather = client.fetch(Weather.self, from: weatherURL) // ⚠️ never awaited
return try await user
// `weather` is cancelled and awaited here automatically at scope exit.
}
Mixing dependent and independent work
func loadProfileScreen(userID: User.ID) async throws -> ProfileScreen {
// These two are independent — start both now.
async let profile = client.fetch(Profile.self, from: profileURL(userID))
async let recentPosts = client.fetch([Post].self, from: postsURL(userID))
// But the friends-of-friends list needs the profile first, so await it,
// then start the dependent work.
let loadedProfile = try await profile
async let mutualFriends = client.fetch([Friend].self,
from: mutualsURL(loadedProfile.friendIDs))
return try await ProfileScreen(
profile: loadedProfile,
posts: recentPosts, // was running concurrently the whole time
mutuals: mutualFriends
)
}
When async let isn't enough
flowchart LR
Q{"How many concurrent
operations?"} Q -->|"Fixed & known
(this + this + this)"| AL["async let"] Q -->|"Dynamic / N from a collection"| TG["Task group (Ch 6)"]
operations?"} Q -->|"Fixed & known
(this + this + this)"| AL["async let"] Q -->|"Dynamic / N from a collection"| TG["Task group (Ch 6)"]
What we built in this chapter
- CityDashboard, which loads three independent resources concurrently with
async let, cutting - The core mechanic:
async letstarts work immediately (as a child task) and continues without - The structured guarantees it brings: children are bound to the scope (no leaks), errors
- How to mix dependent and independent work by placing
awaits at the real data dependencies for - Its one limitation — a fixed, compile-time-known number of operations — and the signpost to **task
Mental model to take away
let x = await f()waits;async let x = f()starts and moves on. That difference is theasync letis structured concurrency: children live and die with the scope, so there's nothing to- Express your dependency graph with
awaitplacement: start everything independent up front, - Reach for
async letfor a fixed, small set of concurrent operations; reach for a task group