Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Handling multiple exceptions from async parallel tasks

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
like image 531
Daniel García Rubio Avatar asked Feb 08 '18 13:02

Daniel García Rubio


1 Answers

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.

like image 119
Evk Avatar answered Oct 14 '22 03:10

Evk