Understanding AggregateException in C# Task Processing
This article provides a clear overview of
AggregateException in C#, explaining what it is, why it
exists, and the specific scenarios in which it occurs during task and
parallel processing. You will also learn how the runtime consolidates
concurrent errors and the proper techniques for inspecting, flattening,
and handling these exceptions in your applications.
What Is an AggregateException?
In C#, AggregateException is a specialized exception
class defined in the System namespace, primarily used by
the Task Parallel Library (TPL) and Parallel LINQ (PLINQ). Because
concurrent operations can execute simultaneously across multiple
threads, multiple independent operations can fail at the same time.
A standard C# method execution can only throw a single exception at a
time. To solve this problem without losing diagnostic details, the .NET
runtime wraps all individual exceptions thrown by parallel or
asynchronous tasks into a single AggregateException
instance. The individual failures are then stored inside its
InnerExceptions collection.
When Does an AggregateException Occur?
An AggregateException occurs whenever code synchronously
waits on or aggregates multiple concurrent operations that encounter one
or more errors. Common scenarios include:
1. Synchronously Blocking on Tasks
When you access .Result or call .Wait() on
a faulted Task or Task<T>, the runtime
wraps the underlying exception in an
AggregateException:
Task task = Task.Run(() => throw new InvalidOperationException("Operation failed."));
try
{
task.Wait(); // Blocking call triggers AggregateException
}
catch (AggregateException ex)
{
// ex.InnerExceptions contains InvalidOperationException
}2. Waiting on Multiple Tasks
Using Task.WaitAll() to synchronously monitor multiple
tasks will produce an AggregateException containing every
exception thrown across all supplied tasks:
Task t1 = Task.Run(() => throw new ArgumentNullException());
Task t2 = Task.Run(() => throw new InvalidOperationException());
try
{
Task.WaitAll(t1, t2);
}
catch (AggregateException ex)
{
// ex.InnerExceptions contains both ArgumentNullException and InvalidOperationException
}3. Parallel Loops and PLINQ
Methods such as Parallel.For,
Parallel.ForEach, and PLINQ queries distribute work across
multiple worker threads. If multiple iterations throw exceptions, the
parallel runner catches them, waits for the iteration state to settle,
and bundles all errors into an AggregateException.
4. Inspecting Completed Tasks
When working with Task.WhenAll, if one or more tasks
fail, the returned task enters the Faulted state. Accessing
the task's .Exception property directly exposes the
AggregateException that contains all inner failures.
Asynchronous
await vs. Synchronous .Wait()
A key distinction in modern C# is the difference between awaiting a task and synchronously blocking:
- Using
await: When youawait task;orawait Task.WhenAll(...), the compiler unwraps theAggregateExceptionand rethrows only the first inner exception encountered. This keeps asynchronous exception handling identical to standard sequentialtry/catchlogic. - Using
.Wait()or.Result: Synchronous blocking does not unwrap the container. Instead, it rethrows the fullAggregateException.
How to Handle an AggregateException
Because tasks can be nested inside other tasks, an
AggregateException can contain other
AggregateException instances. .NET provides dedicated
methods to simplify handling:
Flattening Nested Exceptions
The Flatten() method traverses the exception hierarchy
and creates a new AggregateException containing a single,
flat list of all inner exceptions:
try
{
task.Wait();
}
catch (AggregateException ex)
{
var flatExceptions = ex.Flatten();
foreach (var inner in flatExceptions.InnerExceptions)
{
Console.WriteLine(inner.Message);
}
}Filtering with the Handle Method
The Handle(Func<Exception, bool>) method invokes a
callback for each inner exception. Returning true marks the
exception as handled; returning false leaves it unhandled,
causing the runtime to throw a new AggregateException
containing only the unhandled items:
try
{
Task.WaitAll(task1, task2);
}
catch (AggregateException ex)
{
ex.Handle(inner =>
{
if (inner is TimeoutException)
{
Console.WriteLine("Handled timeout.");
return true;
}
return false; // Any other exception remains unhandled
});
}