9

Problem

Several tasks are run in parallel, and all, none, or any of them might throw exceptions. When all the tasks have finalized, all the exceptions that might have happened must be reported (via log, email, console output.... whatever).

Expected behavior

I can build all the tasks via linq with async lambdas, and then await for them running in parallel with Task.WhenAll(tasks). Then I can catch an AggregateException and report each of the individual inner exceptions.

Actual behavior

An AggregateException is thrown, but it contains just one inner exception, whatever number of individual exceptions have been thrown.

Minimal complete verifiable example

static void Main(string[] args)
{
    try
    {
        ThrowSeveralExceptionsAsync(5).Wait();
    }
    catch (AggregateException ex)
    {
        ex.Handle(innerEx =>
        {
            Console.WriteLine($"\"{innerEx.Message}\" was thrown");
            return true;
        });
    }

    Console.ReadLine();
}

private static async Task ThrowSeveralExceptionsAsync(int nExceptions)
{
    var tasks = Enumerable.Range(0, nExceptions)
        .Select(async n =>
        {
            await ThrowAsync(new Exception($"Exception #{n}"));
        });

    await Task.WhenAll(tasks);
}

private static async Task ThrowAsync(Exception ex)
{
    await Task.Run(() => {
        Console.WriteLine($"I am going to throw \"{ex.Message}\"");
        throw ex;
    });
}

Output

Note that the output order of the "I am going to throw" messages might change, due to race conditions.

I am going to throw "Exception #0"
I am going to throw "Exception #1"
I am going to throw "Exception #2"
I am going to throw "Exception #3"
I am going to throw "Exception #4"
"Exception #0" was thrown

1 Answers1

14

That's because await "unwraps" aggregate exceptions and always throws just first exception (as described in documentation of await), even when you await Task.WhenAll which obviously can result in multiple errors. You can access aggregate exception for example like this:

var whenAll = Task.WhenAll(tasks);
try {
    await whenAll;
}
catch  {
    // this is `AggregateException`
    throw whenAll.Exception;
}

Or you can just loop over tasks and check status and exception of each.

Note that after that fix you need to do one more thing:

try {
    ThrowSeveralExceptionsAsync(5).Wait();
}
catch (AggregateException ex) {
    // flatten, unwrapping all inner aggregate exceptions
    ex.Flatten().Handle(innerEx => {
        Console.WriteLine($"\"{innerEx.Message}\" was thrown");
        return true;
    });
}

Because task returned by ThrowSeveralExceptionsAsync contains AggregateException we thrown, wrapped inside another AggregateException.

Evk
  • 98,527
  • 8
  • 141
  • 191
  • What's the difference between calling `Handle()` on the original `AggregateException` and calling it on the "flattened" one? – cosh Feb 08 '18 at 14:21
  • 3
    @isaac if you don't flatten it - `innerEx` in `Handle` in this case would be another `AggregateException` (one from `Task.WhenAll`), which is kind of useless. Flatten unwraps all inner aggregate exceptions. – Evk Feb 08 '18 at 14:23
  • @Evk Thanks, this solves the problem. Do you happen to know if this is documented somewhere? It is really unintuitive for me, and having to implement this try-catch-throw pattern whenever I expect this behaviour feels weird. I would like to know the reasons behind this design (I'm sure they are good reasons). – Daniel García Rubio Feb 08 '18 at 14:58
  • 1
    @DanielGarcíaRubio yes, it is described right in documentation of `await` keyword (section Exceptions): https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/await. Here is a reasoning from MS employee why they designed await like this: https://social.msdn.microsoft.com/Forums/en-US/e439770e-6c27-40d9-91af-c15d26743a5f/whenall-and-exception?forum=async. If you often await `Task.WhenAll` - you can create extension method and extract repeatitive logic there. – Evk Feb 08 '18 at 15:05
  • Thanks. As always, reasons are good, unintuitive though. I guess that whenever I need to run parallel tasks and report all the exceptions, I should implement this pattern and document that the method will always throw `AggregateException`, as opposed to `WhateverExceptionThatMightHappenFirst`. Then again, if you await such a method you will catch nested `AggregateException`s... – Daniel García Rubio Feb 08 '18 at 15:29
  • @Evk: I am always getting the `OneOrMoreExceptionoccured` as the Exception and the actual exception is present as the inner exception. I need to display the error to user in my application, so is it correct to use inner exception always for that purpose. – Venkat Oct 09 '18 at 04:37
  • @Venkat from your description I guess so. – Evk Oct 10 '18 at 13:40
  • In the first code block, the `catch { throw whenAll.Exception; }` might throw a `NullReferenceException`, because the `Exception` is null for canceled tasks. It's safer to do `catch { whenAll.Wait(); }`. – Theodor Zoulias Feb 06 '23 at 20:16