Getting all exceptions thrown from Parallel.ForEachAsync

 
 
  • Gérald Barré

C# async/await lets you write asynchronous code that reads like synchronous code, including catching specific exception types. This is very convenient, but sometimes you need to capture all exceptions thrown from an async method.

C#
try
{
    // This will throw multiple exception
    await Parallel.ForEachAsync(Enumerable.Range(1, 10), async (i, _) =>
    {
        throw new Exception(i.ToString());
    });
}
catch (Exception ex) // Catch the first exception only, other exceptions are lost
{
    // Prints a value between 1 and 10 depending on which exception was caught first.
    // Other exceptions are lost.
    Console.WriteLine(ex.ToString());
}

To get all thrown exceptions, you need to use Task.Exception or a method that exposes the content of this property, such as Task.Wait.

C#
try
{
    // Use WithAggregateException to get all exceptions
    await Parallel.ForEachAsync(Enumerable.Range(1, 10), async (i, _) =>
    {
        throw new Exception(i.ToString());
    }).WithAggregateException();
}
catch (Exception ex)
{
    // Prints all exceptions as ex is an AggregateException
    Console.WriteLine(ex.ToString());
}

internal static async Task WithAggregateException(this Task task)
{
    // Disable exception throwing using ConfigureAwaitOptions.SuppressThrowing as it
    // will be handled by `task.Wait()`
    await task.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);

    // The task is already completed, so Wait only throws an AggregateException if the task failed
    task.Wait();
}

Note that Parallel.ForEachAsync stops processing items after the first exception is thrown. It waits for any running tasks to complete and then reports all their exceptions. If you run the following code with MaxDegreeOfParallelism = 3, the app prints 1, 2, and 3 to the console before stopping, and the AggregateException contains 3 exceptions:

C#
try
{
    var options = new ParallelOptions() { MaxDegreeOfParallelism = 3 };
    await Parallel.ForEachAsync(Enumerable.Range(1, 100), options, async (i, _) =>
    {
        Console.WriteLine(i); // Prints 1, 2, 3 (in any order), and stops after
        throw new Exception(i.ToString());
    }).WithAggregateException();
}
catch (Exception ex)
{
    // ex contains 3 exceptions (1, 2, 3)
    Console.WriteLine(ex.ToString());
}

To prevent short-circuiting, you can handle exceptions in the delegate and store them:

C#
var exceptions = new ConcurrentBag<Exception>();
var options = new ParallelOptions() { MaxDegreeOfParallelism = 3 };
await Parallel.ForEachAsync(Enumerable.Range(1, 100), options, async (i, _) =>
{
    try
    {
        throw new Exception(i.ToString());
    }
    catch (Exception ex)
    {
        exceptions.Add(ex);
    }
});

if (!exceptions.IsEmpty)
    throw new AggregateException(exceptions);

#Additional resources

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?