Clean&Reactoring Clean Code Created: 09 Feb 2026 Updated: 09 Feb 2026

Handling AggregateException in TPL

When working with the Task Parallel Library (TPL) in C#, you are often running multiple operations simultaneously. While this improves performance, it creates a unique challenge: What happens if multiple tasks fail at the same time?

Standard exception handling usually catches only one error. However, TPL wraps all exceptions from parallel tasks into a single wrapper called AggregateException.

Here are the best practices for handling these complex error scenarios effectively.

1. The Challenge with await and AggregateException

A common misconception is that a simple try-catch block around an await call will catch an AggregateException. In reality, the await keyword unwraps the exception and throws only the first one it finds, discarding the others.

To correctly capture the AggregateException (and thus see all failures), you should assign the task to a variable first, await it safely, and then inspect the Task.Exception property.

public async Task HandleAllExceptionsAsync()
{
var task1 = Task.Run(() => throw new ArgumentException("Error in Task 1"));
var task2 = Task.Run(() => throw new NullReferenceException("Error in Task 2"));
var task3 = Task.Run(() => throw new InvalidOperationException("Error in Task 3"));

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

try
{
await allTasks;
}
catch
{
// 'await' threw the first exception, but 'allTasks.Exception' contains ALL of them.
AggregateException allExceptions = allTasks.Exception;

foreach (var innerException in allExceptions.InnerExceptions)
{
Console.WriteLine($"Caught: {innerException.Message}");
}
}
}

2. Flattening the Hierarchy

AggregateException can be nested. If you have a task that starts another child task, and both fail, you might end up with an AggregateException inside another AggregateException.

Parsing this tree manually is tedious. The .Flatten() method recursively unwrap all nested aggregate exceptions into a single, flat list of errors.

try
{
// Assume 'task' has completed with nested exceptions
await task;
}
catch
{
if (task.Exception != null)
{
// Flattens the nested hierarchy into a single list
var flatExceptions = task.Exception.Flatten().InnerExceptions;

foreach (var innerException in flatExceptions)
{
Console.WriteLine($"Error: {innerException.GetType().Name}");
}
}
}

3. Handling Specific Exception Types

When multiple things go wrong, you often need to react differently to different errors. You might want to retry on a TimeoutException but log and abort on a AuthorizationException.

You can iterate through the inner exceptions and use pattern matching (or the is keyword) to handle them individually.

foreach (var innerException in ex.InnerExceptions)
{
if (innerException is FileNotFoundException)
{
// Specific logic: Maybe create the missing file?
Console.WriteLine("File missing, creating new one...");
}
else if (innerException is UnauthorizedAccessException)
{
// Specific logic: Log a security warning
Console.WriteLine("Access denied. Contact admin.");
}
else
{
// Generic fallback
Console.WriteLine($"Unknown error: {innerException.Message}");
}
}

4. Handling Exceptions "As They Occur" (ContinueWith)

Sometimes you don't want to wait for all tasks to finish (or fail) before you start logging errors. You can attach a continuation using .ContinueWith to handle exceptions the moment a specific task fails.

This approach keeps your main thread free from complex try-catch blocks and delegates error handling to the task itself.

var task1 = SomeAsyncMethod();
var task2 = AnotherAsyncMethod();

await Task.WhenAll(
task1.ContinueWith(t => HandleTaskException(t)),
task2.ContinueWith(t => HandleTaskException(t))
);

// Helper method to process the task result
void HandleTaskException(Task t)
{
// Check if the task faulted (failed)
if (t.Exception != null)
{
foreach (var innerException in t.Exception.InnerExceptions)
{
Console.WriteLine($"Task failed immediately with: {innerException.Message}");
}
}
}

Summary

MethodBest Use Case
Inspect Task.ExceptionWhen you need to see every error that happened during Task.WhenAll.
Flatten()When you have complex, nested tasks and just want a simple list of errors.
ContinueWithWhen you want to log or handle errors immediately without waiting for the whole batch to finish.


Share this lesson: