C#
asynchronous programming
Task.WhenAll
exception handling
.NET

Task.WhenAll not throwing exception as expected

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Task.WhenAll does not throw exceptions at the moment you call it. It creates a task that represents the combined completion of all the supplied tasks. The exception only becomes observable when you await that combined task, wait on it, or inspect its status and exception properties.

That is why developers sometimes think Task.WhenAll is “not throwing.” The returned task has faulted, but nobody has observed that fault yet.

What Task.WhenAll Actually Returns

Task.WhenAll returns a single task that completes when all input tasks have completed. If any input task faults, the combined task becomes faulted.

Example:

csharp
1using System;
2using System.Threading.Tasks;
3
4class Program
5{
6    static async Task Main()
7    {
8        Task t1 = Task.Run(() => throw new InvalidOperationException("First failure"));
9        Task t2 = Task.Run(async () =>
10        {
11            await Task.Delay(100);
12            throw new Exception("Second failure");
13        });
14
15        Task all = Task.WhenAll(t1, t2);
16
17        try
18        {
19            await all;
20        }
21        catch (Exception ex)
22        {
23            Console.WriteLine(ex.Message);
24        }
25    }
26}

The key point is that the exception is observed at await all, not at Task.WhenAll(t1, t2).

Why It May Look Like No Exception Happened

A few patterns create confusion:

  • you call Task.WhenAll(...) but never await it
  • you inspect individual tasks instead of the aggregate task
  • you use fire-and-forget code paths
  • you expect synchronous throwing from an asynchronous composition API

For example, this does not observe the exception properly:

csharp
Task.WhenAll(t1, t2);
Console.WriteLine("Program continues");

The combined task exists, but its failure is never awaited or handled.

await and Multiple Exceptions

Another surprise is that Task.WhenAll can represent multiple task failures, but await does not always present them the way people expect.

The combined task’s Exception property can contain multiple inner exceptions. However, when you await the task, you typically see one propagated exception rather than a manually inspected aggregate list.

If you need all failures explicitly, inspect the returned task after it faults.

csharp
1try
2{
3    await all;
4}
5catch
6{
7    foreach (var ex in all.Exception!.InnerExceptions)
8    {
9        Console.WriteLine(ex.Message);
10    }
11}

This is often the missing step when developers expect “all exceptions” to appear automatically.

WhenAll Still Waits for All Tasks

Task.WhenAll does not fail fast in the sense of immediately abandoning the rest once one task throws. It completes only after all supplied tasks have completed, whether they succeeded, failed, or were canceled.

That behavior is useful when you want full completion and full fault information across a group of operations.

If you need “first task to finish” behavior, that is Task.WhenAny, not Task.WhenAll.

Cancellation Versus Faulting

The resulting task status depends on the input tasks:

  • if any task faults, the combined task faults
  • if none fault but one or more are canceled, the combined task is canceled
  • if all succeed, the combined task succeeds

So a missing exception can also mean the tasks were canceled rather than faulted.

Always check whether you are really dealing with faults and not cancellation tokens ending the operations early.

A Good Diagnostic Pattern

When debugging, keep a reference to the combined task:

csharp
Task all = Task.WhenAll(tasks);

Then inspect:

  • 'all.Status'
  • 'all.Exception'
  • each individual task status if needed

That gives you much more clarity than inlining the call and immediately losing the aggregate object.

Common Pitfalls

One common mistake is expecting Task.WhenAll(...) itself to throw synchronously. It does not. The exception is attached to the returned task.

Another issue is forgetting to await the combined task. Unawaited tasks can make failures appear invisible until much later or not at all in the path you are watching.

It is also easy to expect await to print every failure automatically. If several tasks faulted, inspect all.Exception.InnerExceptions to see the full set.

Finally, do not confuse task cancellation with task failure. A canceled combined task is not the same thing as a faulted one.

Summary

  • 'Task.WhenAll returns a task; it does not throw immediately when called.'
  • Exceptions become observable when you await, wait on, or inspect the returned task.
  • The combined task faults if any input task faults.
  • If multiple tasks fail, inspect Exception.InnerExceptions on the combined task to see the full set.
  • Most confusion comes from not awaiting the aggregate task or from expecting synchronous exception behavior from asynchronous code.

Course illustration
Course illustration

All Rights Reserved.