Running Parallel Async Work with Swift TaskGroup: Patterns, Pitfalls, and Verification
Learn how to run parallel async work with Swift's TaskGroup, handle errors and cancellation correctly, avoid common leaks, and verify the implementation with Swift 6 concurrency checks.
17 Jul 2025, 07:34 UTC

Problem and Takeaway
When you need to execute several asynchronous operations concurrently and collect their results, TaskGroup gives you structured concurrency with automatic cancellation propagation and lifetime management. The key takeaway: always await the group’s result and handle errors inside the group; otherwise child tasks leak and you lose the safety guarantees.
Basic Pattern
The canonical way to run homogeneous async work is withTaskGroup(of:returning:) (or the throwing variant). Child tasks inherit the parent’s priority and task‑local values.
func fetchAll(_ urls: [URL]) async throws -> [Data] {
try await withThrowingTaskGroup(of: Data.self) { group in
for url in urls {
group.addTaskUnlessCancelled {
let (data, _) = try await URLSession.shared.data(from: url)
return data
}
}
var results: [Data] = []
for try await data in group {
results.append(data)
}
return results
}
}
Each addTaskUnlessCancelled checks the group’s cancellation state before starting new work, preventing wasted effort after a failure.
Error Handling and Cancellation Propagation
- If any child throws, the group immediately cancels all remaining children and rethrows the first error.
- When the parent task is cancelled (e.g., the caller’s task is cancelled), every child receives cancellation automatically.
- Long‑running children must cooperate: periodically check
Task.isCancelledor hit a cancellation point such asawait Task.yield().
Common Mistakes
- Discarding the group handle – writing
withTaskGroup { _ in … }without awaiting the returned value leaks child tasks. - Forgetting to
awaitthe group’s result – the caller continues before children finish, breaking structured concurrency. - Assuming completion order –
TaskGroupdoes not guarantee order; usegroup.next()for as‑it‑arrives processing or collect all then sort.
Limitations
- Homogeneous result type only. For heterogeneous results, wrap values in an enum or use separate groups.
addTaskcannot return a value directly; collect viagroup.next(),reduce, or an array as shown above.- Creating thousands of tiny tasks adds scheduler overhead; batch work where possible.
Verification Steps
To confirm the behavior and catch Sendable violations:
- Create a Swift package or playground targeting Swift 5.5+.
- Paste the
fetchAllexample and call it from an@mainasync entry point. - Run with
swift run(orswift playground) to see successful parallel fetches. - Enable Swift 6 language mode for stricter concurrency checking:
The compiler will emit warnings if captured values aren’tswift build -c release -Xswiftc -swift-version -Xswiftc 6Sendableor closures aren’t marked@Sendable. - Optionally, open Instruments → Time Profiler or Points of Interest to visualize task scheduling and cancellation.
Practical Check
After running, verify that the number of returned Data objects matches the input URL count and that no “Task leaked” warnings appear in the console. In Swift 6 mode, a clean build with zero concurrency warnings confirms correct Sendable usage.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.