System.AggregateException

An AggregateException is a wrapper around one or more other exceptions. It is thrown by the Task Parallel Library when a task fails, and by Task.Wait, Task.WaitAll, Task.Result and Parallel.For, which can run many operations at once, and where more than one can fail.

The message is only One or more errors occurred, followed by the messages of the inner exceptions. The cause is in the InnerExceptions property, so that is where to look.

Minimum version: >= 4.0 >= Core 1.0

Statistics

13
elmah.io logo 17

Common causes

The aggregate itself is never the cause. It is thrown because something you ran in the background failed, and these are the ways that reaches your code.

Blocking on a task with Wait or Result

When you block on a task that failed, the exception comes out wrapped in an AggregateException. Even a single failure is wrapped, so catch (InvalidOperationException) around the blocking call does not match.

var task = Task.Run(() => { throw new InvalidOperationException("Product 2 has no price"); });
task.Wait();   // AggregateException wrapping an InvalidOperationException

Await the task instead. await rethrows the original exception, not the wrapper:

var task = Task.Run(new Action(() => { throw new InvalidOperationException("Product 2 has no price"); }));

try
{
    await task;
}
catch (InvalidOperationException e)
{
    Console.WriteLine(e.Message);
}

Task.WaitAll collects the exceptions of every task that failed, so you get one aggregate with one inner exception for each.

var tasks = new[]
{
    Task.Run(new Action(() => throw new InvalidOperationException("Product 2 has no price"))),
    Task.Run(new Action(() => throw new InvalidOperationException("Product 4 has no price")))
};

Task.WaitAll(tasks);   // AggregateException with two inner exceptions

Inspect InnerExceptions, or use await Task.WhenAll and look at the Exception property of the combined task:

var all = Task.WhenAll(Task.Run(new Action(() => throw new InvalidOperationException("Product 2 has no price"))));

try
{
    await all;
}
catch
{
    foreach (var inner in all.Exception!.InnerExceptions)
    {
        Console.WriteLine(inner.Message);
    }
}

Parallel.For and Parallel.ForEach run the body on several threads. If the bodies throw, the loop stops and the exceptions are thrown together as an AggregateException.

Parallel.ForEach(new[] { 1, 2, 3, 4 }, id =>
{
    if (id % 2 == 0) throw new InvalidOperationException($"Product {id} has no price");
});

Catch the aggregate, and handle each inner exception. Or catch inside the loop body, if one failure should not stop the others:

try
{
    Parallel.ForEach(new[] { 1, 2, 3, 4 }, id =>
    {
        if (id % 2 == 0) throw new InvalidOperationException($"Product {id} has no price");
    });
}
catch (AggregateException e)
{
    foreach (var inner in e.InnerExceptions)
    {
        logger.LogError(inner, "Import failed");
    }
}

How to fix it and prevent it

Await instead of blocking

Use async and await all the way. You get the original exception with the right type and stack trace, and no blocked thread. It also removes the risk of a deadlock that comes with .Result in some applications.

var number = await Task.Run(() => 42);

An aggregate can hold many exceptions, and some can be wrapped in aggregates of their own. Flatten() turns a tree into one list, and Handle lets you pick the ones you know how to deal with.

try
{
    Task.WaitAll(Task.Run(new Action(() => throw new TimeoutException())));
}
catch (AggregateException e)
{
    e.Flatten().Handle(inner => inner is TimeoutException);
}

When you log it, log the whole exception, not only e.Message. The message of an aggregate does not say much, and the details are in the inner exceptions that a full log includes.

How to read the stack trace

The first line shows the messages of the inner exceptions. Each inner exception then has its own stack trace below the outer one:

System.AggregateException: One or more errors occurred. (Product 2 has no price) (Product 4 has no price)
 ---> System.InvalidOperationException: Product 2 has no price
   at Shop.Services.CatalogImporter.ImportOne(Int32 id) in C:\src\Shop\Services\CatalogImporter.cs:line 29
   --- End of inner exception stack trace ---
   at System.Threading.Tasks.Task.WaitAllCore(ReadOnlySpan`1 tasks, Int32 millisecondsTimeout, CancellationToken cancellationToken)
   at System.Threading.Tasks.Task.WaitAll(Task[] tasks)
   at Shop.Services.CatalogImporter.ImportAllBlocking(IEnumerable`1 ids) in C:\src\Shop\Services\CatalogImporter.cs:line 23
   at Program.<Main>$(String[] args) in C:\src\Shop\Program.cs:line 6
 ---> (Inner Exception #1) System.InvalidOperationException: Product 4 has no price
   at Shop.Services.CatalogImporter.ImportOne(Int32 id) in C:\src\Shop\Services\CatalogImporter.cs:line 29<---

Here Task.WaitAll on line 23 of CatalogImporter.cs threw an aggregate with two inner exceptions, one per failed product. The line where each task really failed is under ---> and Inner Exception #1, line 29 in ImportOne. Start there.

Should you catch it?

Prefer not to get one in the first place. With await you catch the real exception type, and Task.WhenAll throws the first one while the rest are on the Exception property of the task.

Catch an AggregateException where you have to block, or where you use Parallel, and always look at InnerExceptions to find out what happened.

Find AggregateException before your users do

elmah.io logs every unhandled exception in your .NET application with its stack trace and request details, groups identical errors and notifies you when a new one appears.

Start free trial
Free 21-day trial No credit card required

Related exceptions

  • TaskCanceledException is what you find inside the aggregate when a task was canceled.
  • TimeoutException is a common inner exception when an operation takes too long.
  • Exception is the base class of every exception, including the aggregate.

Frequently asked questions

Why is the exception wrapped in an AggregateException?

A task can have more than one failure, for example when it has child tasks. The blocking methods always wrap them, so the type is the same however many failed.

No. await rethrows the first inner exception directly. If you need all of them from Task.WhenAll, read the Exception property of the task that was awaited.

Use e.InnerException for the first one, or e.GetBaseException() to get the innermost. Use InnerExceptions when there can be more than one.

Not when you await it, only when you call .Wait() or .Result, or read the Exception property of a task from Task.WhenAll.

Let your AI agent track it down

Connect Claude Code, Cursor, VS Code or Visual Studio to the elmah.io MCP server and ask your agent to look into AggregateException for you. For example:

AI chat
Find the most frequent AggregateException in my production log and show me the line that throws it.

The agent reads the stack trace and request details from elmah.io, finds the code in your repository and proposes a fix. In Claude Code, add the server with one command:

claude mcp add --transport http --client-id claudecode elmahio https://mcp.elmah.io/mcp

The MCP server is included on every plan and is currently in beta. Set up the MCP server.

Further reading

YouTube videos

Answers from Stack Overflow

Instead of implementing retry functionality that wraps the HttpClient, consider constructing the HttpClient with a HttpMessageHandler that performs the retry logic internally. For example:

public class RetryHandler : DelegatingHandler
{
    // Strongly consider limiting the number of retries - "retry forever" is
    // probably not the most user friendly way you could respond to "the
    // network cable got pulled out."
    private const int MaxRetries = 3;

    public RetryHandler(HttpMessageHandler innerHandler)
        : base(innerHandler)
    { }

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        HttpResponseMessage response = null;
        for (int i = 0; i < MaxRetries; i++)
        {
            response = await base.SendAsync(request, cancellationToken);
            if (response.IsSuccessStatusCode) {
                return response;
            }
        }

        return response;
    }
}

public class BusinessLogic
{
    public void FetchSomeThingsSynchronously()
    {
        // ...

        // Consider abstracting this construction to a factory or IoC container
        using (var client = new HttpClient(new RetryHandler(new HttpClientHandler())))
        {
            myResult = client.PostAsync(yourUri, yourHttpContent).Result;
        }

        // ...
    }
}

By Dan Bjorge. Read the original answer on Stack Overflow.

ASP.NET Core 2.1 Answer

ASP.NET Core 2.1 added support for Polly directly. Here UnreliableEndpointCallerService is a class which accepts a HttpClient in its constructor. Failed requests will retry with an exponential back-off so that the next retry takes place in an exponentially longer time after the previous one:

services
    .AddHttpClient<UnreliableEndpointCallerService>()
    .AddTransientHttpErrorPolicy(
        x => x.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(3, retryAttempt))));

Also, consider reading my blog post "Optimally Configuring HttpClientFactory".

Other Platforms Answer

This implementation uses Polly to retry with an exponential back-off so that the next retry takes place in an exponentially longer time after the previous one. It also retries if a HttpRequestException or TaskCanceledException is thrown due to a timeout. Polly is much easier to use than Topaz.

public class HttpRetryMessageHandler : DelegatingHandler
{
    public HttpRetryMessageHandler(HttpClientHandler handler) : base(handler) {}

    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken) =>
        Policy
            .Handle<HttpRequestException>()
            .Or<TaskCanceledException>()
            .OrResult<HttpResponseMessage>(x => !x.IsSuccessStatusCode)
            .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(3, retryAttempt)))
            .ExecuteAsync(() => base.SendAsync(request, cancellationToken));
}

using (var client = new HttpClient(new HttpRetryMessageHandler(new HttpClientHandler())))
{
    var result = await client.GetAsync("http://example.com");
}

By Muhammad Rehan Saeed. Read the original answer on Stack Overflow.

It is, it's all about understanding the lifetimes of the various elements in play and getting those set correctly.

By default the DbContextFactory created by the AddDbContextFactory() extension method has a Singleton lifespan. If you use the AddDbContext() extension method with it's default settings it will create a DbContextOptions with a Scoped lifespan (see the source-code here), and as a Singleton can't use something with a shorter Scoped lifespan, an error is thrown.

To get round this, we need to change the lifespan of the DbContextOptions to also be 'Singleton'. This can be done using by explicitly setting the scope of the DbContextOptions parameter of AddDbContext()

services.AddDbContext<FusionContext>(options =>
    options.UseSqlServer(YourSqlConnection),
    optionsLifetime: ServiceLifetime.Singleton);

There's a really good discussion of this on the EF core GitHub repository starting here. It's also well worth having a look at the source-code for DbContextFactory here.

Alternatively, you can also change the lifetime of the DbContextFactory by setting the ServiceLifetime parameter in the constructor:

services.AddDbContextFactory<FusionContext>(options => 
    options.UseSqlServer(YourSqlConnection), 
    ServiceLifetime.Scoped);

The options should be configured exactly as you would for a normal DbContext as those are the options that will be set on the DbContext the factory creates.

By tomRedox. Read the original answer on Stack Overflow.

As the message says, you have a task which threw an unhandled exception.

Turn on Break on All Exceptions (Debug, Exceptions) and rerun the program.
This will show you the original exception when it was thrown in the first place.


(comment appended): In VS2015 (or above). Select Debug > Options > Debugging > General and unselect the "Enable Just My Code" option.

By SLaks. Read the original answer on Stack Overflow.

The difference here comes from using token.ThrowIfCancellationRequested(). This method checks for cancellation and if requested throws OperationCanceledException specifically and not TaskCanceledException (understandable as CancellationToken isn't exclusive to the TPL). You can look at the reference source and see that it calls this method:

private void ThrowOperationCanceledException()
{
    throw new OperationCanceledException(Environment.GetResourceString("OperationCanceled"), this);
}

"Regular" cancellation though will indeed generate a TaskCanceledException. You can see that by cancelling the token before the task had a chance to start running:

cancellationTokenSource.Cancel();
var task = Task.Run(() => { }, cancellationTokenSource.Token);
try
{
    await task; 
}
catch (Exception ex)
{
    Console.WriteLine(ex.ToString());
    Console.WriteLine($"Task.IsCanceled: {task.IsCanceled}");
    Console.WriteLine($"Task.IsFaulted: {task.IsFaulted}");
    Console.WriteLine($"Task.Exception: {((task.Exception == null) ? "null" : task.Exception.ToString())}");
}

Output:

System.Threading.Tasks.TaskCanceledException: A task was canceled.
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
   at Sandbox.Program.<MainAsync>d__1.MoveNext()
Task.IsCanceled: True
Task.IsFaulted: False
Task.Exception: null

Traditional .Net methods usually don't use CancellationToken.ThrowIfCancellationRequested for async API as this is only appropriate when offloading work to another thread. These methods are for inherently asynchronous operations so cancellation is monitored using CancellationToken.Register (or the internal InternalRegisterWithoutEC).

By i3arnon. Read the original answer on Stack Overflow.