What is withThrowingDiscardingTaskGroup?
withThrowingDiscardingTaskGroup is part of Swift’s structured concurrency API, added in Swift 5.9 via SE-0381. It’s the throwing sibling of withDiscardingTaskGroup, and the whole family looks like this: withTaskGroup (non-throwing, keeps results), withThrowingTaskGroup (throwing, keeps results), withDiscardingTaskGroup (non-throwing, discards results), withThrowingDiscardingTaskGroup (throwing, discards results).
The problem it solves
A regular withThrowingTaskGroup retains bookkeeping for every child task until you drain it with next() or by iterating. That’s fine when a group is bounded — spawn 20 tasks, await them all, done. But if you use a task group as a long-lived supervisor — say, a server loop that spawns a new child task per incoming connection or request, indefinitely, where the tasks are just doing side-effecting work and returning Void — that internal per-task record keeps accumulating because nothing ever calls next() to consume completed tasks. You get unbounded memory growth in a process that’s supposed to run forever. Discarding task groups exist specifically for that “fire-and-forget, unbounded lifetime” shape: as each child finishes, its result and metadata are eagerly discarded rather than buffered for later retrieval.
Two consequences that follow from the design
These are the details that separate a surface-level answer from a strong one.
1. No result retrieval. Because results are discarded, there’s no next() and no AsyncSequence conformance — you literally can’t retrieve individual task outcomes. The type system enforces this by requiring addTask’s closure to return Void. That also means the common “bounded concurrency” pattern people use with regular task groups — add N tasks, then loop calling next() and adding one more each time something completes, to cap concurrency at N — is not possible with a discarding group, since it depends on observing individual completions. If you need that pattern, you’re back to withThrowingTaskGroup.
2. Fail-fast error and cancellation semantics. In a normal withThrowingTaskGroup, if a child throws, that error just sits there until you call next() and observe it — other children keep running unaffected unless you explicitly call group.cancelAll(). In withThrowingDiscardingTaskGroup, an unhandled error thrown by any child automatically cancels the whole group (all sibling tasks see Task.isCancelled == true), and once the outstanding tasks unwind, that error is rethrown from the with... call itself. It’s fail-fast by default, which fits its role as a supervisor: one broken child tears down the batch rather than silently leaking a failure nobody’s watching for.
Structured concurrency’s core guarantee still holds: the function doesn’t return until all child tasks have completed, and cancellation propagates from the parent task into the group. It’s just that “waiting for completion” no longer means “waiting so you can collect results.”
Minimal example
try await withThrowingDiscardingTaskGroup { group in
for connection in incomingConnections {
group.addTask {
try await handle(connection) // returns Void
}
}
}
When it’s actually relevant on iOS
For iOS specifically, this rarely comes up in typical app code — loading 20 thumbnails, fetching a handful of endpoints for a screen — because that work is bounded and you usually do want the results, so withThrowingTaskGroup or plain async let is the right tool. withThrowingDiscardingTaskGroup is more relevant to long-running, server-like or daemon-like components: a background sync engine, a socket/event loop, a continuously-running job processor inside the app that spawns void, side-effecting work over the app’s whole session.
What a strong interview answer hits
- What problem it solves — memory growth in unbounded fire-and-forget task groups.
- The structural constraint that follows —
Void-only results, nonext(). - The behavioral difference that follows — fail-fast cancellation on first error vs. observe-on-drain.
- A judgment call about when you’d actually reach for it vs. a regular task group.
That last point — knowing it’s the wrong tool for bounded-concurrency-pool patterns — is usually what distinguishes someone who read the changelog from someone who’s actually reasoned about the tradeoffs.