Sequential await vs async let

Concurrency Mid-level Swift 5.7+ iOS 16+ Reviewed By Alok Singh

Predict the timing

Each fetch suspends for about 1 second and they don't depend on each other. Roughly how long does each version take from start to finish?

Given: fetchA() and fetchB() each await Task.sleep(for: .seconds(1)), then return a string.

Show the code
func fetchA() async -> String {
    try? await Task.sleep(for: .seconds(1))
    return "A"
}
func fetchB() async -> String {
    try? await Task.sleep(for: .seconds(1))
    return "B"
}
V1
let a = await fetchA()
let b = await fetchB()
V2
let (x, y) = await (fetchA(),
                    fetchB())
V3
async let p = fetchA()
async let q = fetchB()
let (r, s) = await (p, q)
Options
Hint 1 of 2

Which line actually starts a new child task?

Hint 2 of 2

A tuple's elements are evaluated in order, left to right.

Choose an answer, then open Show answer below to check it.

Show answer

Answer: B V1 2 s · V2 2 s · V3 1 s

Correct. Only async let starts both child tasks before anything is awaited.

V1 and V2 run the calls one after another (about 2 seconds). V3 uses async let, which starts each call as a child task immediately, so both sleep at the same time (about 1 second). For a list of calls whose length is only known at runtime, use a TaskGroup.

Why not the others?
  • A V1 2 s · V2 1 s · V3 1 s. The tuple in V2 looks parallel but is not. Its elements are evaluated left to right, and each call is awaited before the next one starts.
  • C All 1 s. await suspends the current task until that call finishes, so V1 runs the calls one after another.
  • D All 2 s. async let creates a child task right away, so both fetches overlap in V3.
Timeline on one shared 0–2 second axis: V1 and V2 run fetchA then fetchB and finish at 2 s; V3 runs both at once and finishes at 1 s.0 s0.5 s1 s1.5 s2 sVersion 1: sequential awaitfetchAfetchB2 sVersion 2: tuple of awaitsfetchAfetchB2 sVersion 3: async letfetchA (child task)fetchB (child task)1 s
Timeline on one shared 0–2 second axis: V1 and V2 run fetchA then fetchB and finish at 2 s; V3 runs both at once and finishes at 1 s.
Text version
Timeline as text, elapsed time t in seconds
VersionWhat runs whenFinishes at
V1
  • fetchA from t = 0.0 s to t = 1.0 s
  • fetchB from t = 1.0 s to t = 2.0 s
t = 2.0 s
V2
  • fetchA from t = 0.0 s to t = 1.0 s
  • fetchB from t = 1.0 s to t = 2.0 s
t = 2.0 s
V3
  • fetchA (child task) from t = 0.0 s to t = 1.0 s
  • fetchB (child task) from t = 0.0 s to t = 1.0 s
t = 1.0 s

30-second answer

Plain await runs calls one after another, and so does a tuple of awaited calls. async let starts each call as a child task straight away, so independent work overlaps and the total time is close to the slowest call rather than the sum.

Practise saying it · 45 s. No recording, just a timer

Explain to an interviewer when you'd use async let versus a TaskGroup.

Say your answer out loud, then check which of these points you covered:

  • Says async let suits a fixed, small number of independent calls
  • Says TaskGroup suits a number of tasks only known at runtime
  • Mentions that child tasks are cancelled and awaited when the scope exits
  • Points out that a tuple of awaits is sequential

Deep dive

await marks a suspension point. The task waits for that call to finish before moving to the next line.

async let x = work() creates a child task immediately. Awaiting x later only collects the result.

If the scope exits before an async let is awaited (for example, because an error was thrown), the child task is cancelled and awaited implicitly. Structured concurrency never leaks it.

Use a TaskGroup (withTaskGroup / withThrowingTaskGroup) when the number of tasks depends on runtime data, such as one fetch per ID.

Common mistakes

  • Believing await (a(), b()) runs both calls in parallel.
  • Using Task {} for each call and losing structured cancellation.
  • Starting dependent calls with async let when the second needs the first's result.

Senior take & follow-ups

Strong candidates explain the timeline, not just the syntax: what starts when, what is awaited, and what happens to the remaining work when something fails.

Follow-up questions

Think of your answer first, then open the hint.

  1. What happens to q if p throws?

    Show hint

    When the scope exits, q is cancelled and awaited before the error propagates.

  2. How would you fetch 50 user IDs in parallel but at most 5 at a time?

    Show hint

    Use a TaskGroup and add new tasks only as earlier ones finish.

Suggest a fix if something here is wrong or out of date.