Parallelism with automatic cleanup and cancellation is what makes Swift concurrency safer than raw threads or GCD.
Structured concurrency means child tasks live inside a scope: the parent can't finish until all its children have, errors propagate up, and cancelling the parent cancels the children.
`async let` starts a child task immediately and lets you await its result later:
async let profile = loadProfile()
async let posts = loadPosts()
let page = try await Page(profile: profile, posts: posts)Both loads run in parallel. If you leave the scope without awaiting one, it is cancelled and awaited implicitly.
For a dynamic number of children, use a task group: withTaskGroup(of:) or withThrowingTaskGroup(of:). Inside, group.addTask { ... } spawns children and for await result in group collects results in completion order. In a throwing group, the first error thrown out of the body cancels the remaining children.
Cancellation is cooperative: cancelling only sets a flag. Long-running code must check it with try Task.checkCancellation() (throws CancellationError) or Task.isCancelled. Many system APIs such as Task.sleep and URLSession already throw when cancelled. Use withTaskCancellationHandler to react immediately, e.g. to cancel a callback-based operation.
Task { } and Task.detached { } create unstructured tasks that are not tied to the current scope; you must manage and cancel them yourself.
Example
func square(_ n: Int) async throws -> Int {
try await Task.sleep(for: .milliseconds(100 * n))
try Task.checkCancellation()
return n * n
}
func sumOfSquares(_ ns: [Int]) async throws -> Int {
try await withThrowingTaskGroup(of: Int.self) { group in
for n in ns { group.addTask { try await square(n) } }
var total = 0
for try await value in group { total += value } // completion order
return total
}
}
async let a = square(2)
async let b = square(3)
print(try await a + b) // 13, in about 0.3 s not 0.5 s
print(try await sumOfSquares([1, 2, 3])) // 14
let slow = Task { try await sumOfSquares([10, 20]) }
slow.cancel()
print(await slow.result) // failure(CancellationError())Remember
async letand task groups run children in parallel within a scope- Group results arrive in completion order
- Cancellation is cooperative — check it in long loops
Watch out for
- Spawning
Task { }inside loops instead of a task group, losing cancellation and error propagation.