Lesson 19 / 25
async/await and Structured Concurrency
Write asynchronous code with async/await, async let and task groups.
Asynchronous code that reads like synchronous code
Swift's concurrency model (Swift 5.5+) replaces nested completion handlers with async/await. An async function can suspend at each await, freeing its thread to do other work, and resume when the awaited operation completes; the code still reads top to bottom. Errors work naturally with async throws and try await. Structured concurrency runs work in parallel with clear lifetimes: async let starts child tasks that run concurrently and are awaited later, and withTaskGroup or withThrowingTaskGroup handle a dynamic number of child tasks. Child tasks cannot outlive their parent scope, and cancellation and errors propagate automatically. Task { } starts new top-level asynchronous work from synchronous code (for example from a button action), inheriting the current actor context, while Task.detached does not. Apple's frameworks offer async APIs such as URLSession.data(from:). Avoid blocking threads inside async code with semaphores or sleeps; use try await Task.sleep(for: .seconds(1)) when you need a delay.
Parallel child tasks
A parent task starts several child tasks, awaits them all, and they cannot outlive it.
Fetching several resources concurrently
async let runs requests in parallel; the function awaits them together.
import Foundation
struct Product: Decodable { let id: String; let title: String }
struct Review: Decodable { let rating: Int; let text: String }
func fetch<T: Decodable>(_ type: T.Type, from url: URL) async throws -> T {
let (data, response) = try await URLSession.shared.data(from: url)
guard (response as? HTTPURLResponse)?.statusCode == 200 else { throw URLError(.badServerResponse) }
return try JSONDecoder().decode(T.self, from: data)
}
func loadProductPage(id: String) async throws -> (Product, [Review]) {
let base = URL(string: "https://api.example.com")!
async let product = fetch(Product.self, from: base.appending(path: "products/\(id)"))
async let reviews = fetch([Review].self, from: base.appending(path: "products/\(id)/reviews"))
return try await (product, reviews) // both requests run concurrently
}
func loadPrices(for ids: [String]) async throws -> [String: Int] {
try await withThrowingTaskGroup(of: (String, Int).self) { group in
for id in ids {
group.addTask { (id, try await fetchPrice(id)) }
}
var prices: [String: Int] = [:]
for try await (id, price) in group { prices[id] = price }
return prices
}
}await is a suspension point
Anything can change while a function is suspended at await: other code may run and modify shared state. Re-check assumptions after an await rather than relying on values read before it.
Quick check: What does `async let` do?
- Starts a child task that runs concurrently and is awaited later
- Runs code synchronously
- Creates a detached task with no parent
- Blocks the thread until done
Answer
Starts a child task that runs concurrently and is awaited later — async let creates a concurrent child task bound to the current scope.