Handle AggregateException in C# Parallel Processing

Parallel operations in .NET aggregate multiple runtime errors into a single AggregateException object rather than throwing individual errors sequentially. This article explains how to intercept, unwrap, and resolve these errors when parallel processing fails using constructs such as Parallel.ForEach, PLINQ, and Task.WhenAll. You will learn how to access the InnerExceptions collection, flatten deeply nested exception hierarchies, and selectively handle known failure conditions while propagating unexpected errors.

Understanding AggregateException

When running operations concurrently across multiple threads, more than one thread can fail simultaneously. To preserve the stack traces and details of every failure, the .NET runtime collects each error and encapsulates them inside the System.AggregateException class. The individual exceptions are exposed via the InnerExceptions read-only collection.

Catching and Inspecting Inner Exceptions

When invoking synchronous parallel APIs like Parallel.For or Parallel.ForEach, any unhandled error triggers an AggregateException immediately upon completion or cancellation.

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

var items = new List<int> { 1, 2, 3, 4, 5 };

try
{
    Parallel.ForEach(items, item =>
    {
        if (item % 2 == 0)
        {
            throw new InvalidOperationException($"Failure on item {item}");
        }
    });
}
catch (AggregateException ae)
{
    foreach (var inner in ae.InnerExceptions)
    {
        Console.WriteLine($"Caught: {inner.GetType().Name} - {inner.Message}");
    }
}

Flattening Nested Hierarchies

Complex asynchronous workflows or nested parallel loops often produce an AggregateException containing child AggregateException instances. Navigating this tree manually adds unnecessary complexity.

Call the Flatten() method to collapse all nested exceptions into a single AggregateException containing a one-dimensional list of base exceptions:

try
{
    // A task that spawns child tasks or nested parallel calls
    Task parentTask = Task.Run(() =>
    {
        Parallel.Invoke(
            () => throw new TimeoutException("Operation timed out."),
            () => throw new ArgumentException("Invalid parameter.")
        );
    });

    parentTask.Wait();
}
catch (AggregateException ae)
{
    var flatExceptions = ae.Flatten();

    foreach (var inner in flatExceptions.InnerExceptions)
    {
        Console.WriteLine($"Flattened: {inner.GetType().Name}: {inner.Message}");
    }
}

Selective Handling with the Handle Method

The AggregateException.Handle method allows you to evaluate each inner exception individually using a predicate. If the predicate returns true, that specific exception is marked as handled. If any exception in the list returns false, a new AggregateException is immediately rethrown containing only the unhandled exceptions.

try
{
    Parallel.ForEach(items, item =>
    {
        if (item == 2) throw new TimeoutException("Network timeout.");
        if (item == 4) throw new OutOfMemoryException("Memory exhausted.");
    });
}
catch (AggregateException ae)
{
    ae.Flatten().Handle(ex =>
    {
        if (ex is TimeoutException)
        {
            Console.WriteLine("Recovered from TimeoutException.");
            return true; // Marked as handled
        }

        // Return false for any exception that should bubble up
        return false;
    });
}

Handling AggregateException in Async Code (Task.WhenAll)

When using await Task.WhenAll(...), the await keyword automatically rethrows only the first inner exception that occurred, suppressing the rest from the immediate catch block.

To inspect every failure when parallel asynchronous tasks fail, capture the Task object directly:

var task1 = Task.FromException(new InvalidOperationException("Task 1 failed"));
var task2 = Task.FromException(new ArgumentException("Task 2 failed"));

Task allTasks = Task.WhenAll(task1, task2);

try
{
    await allTasks;
}
catch
{
    // Retrieve all aggregated failures from the Task object itself
    if (allTasks.Exception != null)
    {
        foreach (var inner in allTasks.Exception.Flatten().InnerExceptions)
        {
            Console.WriteLine($"Async Error: {inner.Message}");
        }
    }
}