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 you await task; or await Task.WhenAll(...), the compiler unwraps the AggregateException and rethrows only the first inner exception encountered. This keeps asynchronous exception handling identical to standard sequential try/catch logic.
  • Using .Wait() or .Result: Synchronous blocking does not unwrap the container. Instead, it rethrows the full AggregateException.

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
    });
}