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