1

When causing multiple exceptions in a Task.WhenAll call, it looks like only one of the exceptions is absorbed into the Task once you await it through more than one layer of awaiting. I was under the impression that the Task.Exception.InnerExceptions property would contain all exceptions that occurred, but under certain circumstances they seem to only have one.

For example, this sample code creates multiple exception-throwing Tasks and then awaits a Task.WhenAll on them, and then writes to console the exceptions that it is able to catch:

class Program
{
    static async Task Main(string[] args)
    {
        var task = CauseMultipleExceptionsAsync();

        // Delaying until all the Exceptions have been thrown, ensuring it isn't just a weird race condition happening behind the scenes
        await Task.Delay(5000);

        try
        {
            await task;
        }
        catch(AggregateException e)
        {
            // This does not get hit
            Console.WriteLine($"AggregateException caught: Found {e.InnerExceptions.Count} inner exception(s)");
        }
        catch(Exception e)
        {
            Console.WriteLine($"Caught other Exception {e.Message}");

            Console.WriteLine($"task.Exception.InnerExceptions contains {task.Exception.InnerExceptions.Count} exception(s)");
            foreach (var exception in task.Exception.InnerExceptions)
            {
                Console.WriteLine($"Inner exception {exception.GetType()}, message: {exception.Message}");
            }
        }
    }

    static async Task CauseMultipleExceptionsAsync()
    {
        var tasks = new List<Task>()
        {
            CauseExceptionAsync("A"),
            CauseExceptionAsync("B"),
            CauseExceptionAsync("C"),
        };

        await Task.WhenAll(tasks);
    }

    static async Task CauseExceptionAsync(string message)
    {
        await Task.Delay(1000);
        Console.WriteLine($"Throwing exception {message}");
        throw new Exception(message);
    }
}

I was expecting this to either enter the catch(AggregateException e) clause, or at least to have three inner exceptions in task.Exception.InnerExceptions - what actually happens that one one exception is raised, and only only one of the exceptions is in task.Exception.InnerExceptions:

Throwing exception B
Throwing exception A
Throwing exception C
Caught other Exception A
task.Exception.InnerExceptions contains 1 exception(s)
Inner exception System.Exception, message: A

What is weirder is that this behaviour changes depending on whether you await the Task.WhenAll call in CauseMultipleExceptionsAsync - if you return the Task directly rather than awaiting it, then all three exceptions appear in task.Exception.InnerException. For instance, replacing CauseMultipleExceptionsAsync with this:

    static Task CauseMultipleExceptionsAsync()
    {
        var tasks = new List<Task>()
        {
            CauseExceptionAsync("A"),
            CauseExceptionAsync("B"),
            CauseExceptionAsync("C"),
        };

        return Task.WhenAll(tasks);
    }

Gives this result, with all three exceptions contained in task.Exception.InnerExceptions:

Throwing exception C
Throwing exception A
Throwing exception B
Caught other Exception A
task.Exception.InnerExceptions contains 3 exception(s)
Inner exception System.Exception, message: A
Inner exception System.Exception, message: B
Inner exception System.Exception, message: C

I'm quite confused about this - where did exceptions B and C go in the initial example? How would you go about finding them again if Task.Exception doesn't contain any information about them? Why does awaiting inside CauseMultipleExceptionsAsync hide these exceptions, while returning the Task.WhenAll directly does not?

If it makes a difference, I am able to replicate the above in both .Net Framework 4.5.2 and .Net Core 2.1.

John
  • 500
  • 4
  • 15
  • 1
    Take a look at this. https://stackoverflow.com/questions/18314961/i-want-await-to-throw-aggregateexception-not-just-the-first-exception – Stack Undefined Apr 22 '20 at 17:13
  • Thanks - that answers why the catch(AggregateException) clause isn't entered but I'm not sure it answers the main question of why InnerExceptions sometimes contains and sometimes doesn't contain all the exceptions thrown. – John Apr 22 '20 at 19:21

1 Answers1

2

What you are observing is the behavior of the await operator, not the behavior of the Task.WhenAll method. If you are interested in why the await behaves this way, you could read this article from the early days of async/await:

Having the choice of always throwing the first or always throwing an aggregate, for await we opt to always throw the first. This doesn’t mean, though, that you don’t have access to the same details. In all cases, the Task’s Exception property still returns an AggregateException that contains all of the exceptions, so you can catch whichever is thrown and go back to consult Task.Exception when needed. Yes, this leads to a discrepancy between exception behavior when switching between task.Wait() and await task, but we’ve viewed that as the significant lesser of two evils.

In case you would like to implement a method similar in behavior to Task.WhenAll, but without losing the convenience of the async/await machinery, it is tricky but there are workarounds available here.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104