Why doesn't await on Task.WhenAll throw an AggregateException?
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
Many developers expect await Task.WhenAll(...) to throw AggregateException, but await follows different exception semantics. The language unpacks task failures to keep async code readable and close to synchronous try and catch flow. The aggregate is still present on the task object, but not always thrown directly by await.
How await Handles Exceptions
await observes the awaited task and rethrows failures in an unwrapped form when possible.
This behavior is by design so ordinary async error handling does not require aggregate unwrapping in common cases.
What Task.WhenAll Produces
Task.WhenAll returns one task that completes when all inner tasks complete. If multiple inner tasks fail, the returned task stores all failures in its Exception.InnerExceptions collection.
So the aggregate exists, but await surfaces a simplified throw path.
Why This Design Is Useful
If every awaited failure always threw AggregateException, most async catch blocks would include boilerplate unwrapping logic for single-failure operations. The current model optimizes everyday readability while still allowing advanced inspection.
You get two levels:
- simple thrown exception from
awaitfor primary control flow, - full failure list from the stored aggregate when needed.
Difference From .Wait() And .Result
Blocking APIs use older task consumption semantics.
This is one reason async codebases should avoid .Wait() and .Result on asynchronous tasks.
Cancellation And Mixed Outcomes
When tasks include cancellation and faults together, final status can be faulted or canceled depending on combined outcomes. For incident analysis, capture:
- overall task status,
- first thrown exception observed by
await, - full
InnerExceptionslist when present, - cancellation token state.
This gives complete telemetry for parallel workflows.
Practical Logging Pattern
In service code, preserve the WhenAll task reference, then log all inner failures in catch block.
This pattern avoids losing secondary failures.
Unit Testing This Behavior
When documenting team conventions, add tests that assert both the thrown exception from await and the full InnerExceptions list on the aggregate task. This prevents future refactors from silently reducing error visibility in parallel execution paths.
Common Pitfalls
- Assuming
awaithides additional failures fromTask.WhenAll. - Discarding
WhenAlltask reference and losing aggregate inspection path. - Using
.Resultin async paths and introducing aggregate wrapping plus deadlock risk. - Logging only first observed exception in parallel workloads.
- Ignoring cancellation state when diagnosing mixed outcomes.
Summary
- '
awaitfavors readable exception flow and often rethrows a single exception type directly.' - '
Task.WhenAllstill captures all failures in aggregate task state.' - Keep a reference to the returned task to inspect
InnerExceptions. - Prefer
awaitover blocking task consumption APIs. - Log aggregated failures explicitly when debugging parallel task orchestration.

