Getting all exceptions thrown from Parallel.ForEachAsync

 
 
  • Gérald Barré

In C# you can write async code that looks like synchronous code. People using async/await expect to be able to catch specific exception types as synchronous code. This is very convenient, but sometimes you want to get 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());
}

If you want to get all thrown exceptions, you need to use Task.Exceptions or a method that exposes the content of this method, 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 from the collection after the first exception is thrown. It waits for the current tasks and then reports the exceptions. If you run the following code, 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());
}

If you want to prevent short-circuiting, you can use 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?Buy Me A Coffee💖 Sponsor on GitHub