Why doesn't await on Task.WhenAll throw an Agg

2020-02-17 03:48发布

问题:

In this code:

private async void button1_Click(object sender, EventArgs e) {
    try {
        await Task.WhenAll(DoLongThingAsyncEx1(), DoLongThingAsyncEx2());
    }
    catch (Exception ex) {
        // Expect AggregateException, but got InvalidTimeZoneException
    }
}

Task DoLongThingAsyncEx1() {
    return Task.Run(() => { throw new InvalidTimeZoneException(); });
}

Task DoLongThingAsyncEx2() {
    return Task.Run(() => { throw new InvalidOperation();});
}

I expected WhenAll to create and throw an AggregateException, since at least one of the tasks it was awaiting on threw an exception. Instead, I'm getting back a single exception thrown by one of the tasks.

Does WhenAll not always create an AggregateException?

回答1:

I don't exactly remember where, but I read somewhere that with new async/await keywords, they unwrap the AggregateException into the actual exception.

So, in catch block, you get the actual exception and not the aggregated one. This helps us write more natural and intuitive code.

This was also needed for easier conversion of existing code into using async/await where the a lot of code expects specific exceptions and not aggregated exceptions.

-- Edit --

Got it:

An Async Primer by Bill Wagner

Bill Wagner said: (in When Exceptions Happen)

...When you use await, the code generated by the compiler unwraps the AggregateException and throws the underlying exception. By leveraging await, you avoid the extra work to handle the AggregateException type used by Task.Result, Task.Wait, and other Wait methods defined in the Task class. That’s another reason to use await instead of the underlying Task methods....



回答2:

I know this is a question that's already answered but the chosen answer doesn't really solve the OP's problem, so I thought I would post this.

This solution gives you the aggregate exception (i.e. all the exceptions that were thrown by the various tasks) and doesn't block (workflow is still asynchronous).

async Task Main()
{
    var task = Task.WhenAll(A(), B());

    try
    {
        var results = await task;
        Console.WriteLine(results);
    }
    catch (Exception)
    {
        if (task.Exception != null)
        {
            throw task.Exception;
        }
    }
}

public async Task<int> A()
{
    await Task.Delay(100);
    throw new Exception("A");
}

public async Task<int> B()
{
    await Task.Delay(100);
    throw new Exception("B");
}

The key is to save a reference to the aggregate task before you await it, then you can access its Exception property which holds your AggregateException (even if only one task threw an exception).

Hope this is still useful. I know I had this problem today.



回答3:

You can traverse all tasks to see if more than one have thrown an exception:

private async Task Example()
{
    var tasks = new [] { DoLongThingAsyncEx1(), DoLongThingAsyncEx2() };

    try 
    {
        await Task.WhenAll(tasks);
    }
    catch (Exception ex) 
    {
        var exceptions = tasks.Where(t => t.Exception != null)
                              .Select(t => t.Exception);
    }
}

private Task DoLongThingAsyncEx1()
{
    return Task.Run(() => { throw new InvalidTimeZoneException(); });
}

private Task DoLongThingAsyncEx2()
{
    return Task.Run(() => { throw new InvalidOperationException(); });
}


回答4:

You're thinking of Task.WaitAll - it throws an AggregateException.

WhenAll just throws the first exception of the list of exceptions it encounters.



回答5:

Just thought I'd expand on @Richiban's answer to say that you can also handle the AggregateException in the catch block by referencing it from the task. E.g:

async Task Main()
{
    var task = Task.WhenAll(A(), B());

    try
    {
        var results = await task;
        Console.WriteLine(results);
    }
    catch (Exception ex)
    {
        // This doesn't fire until both tasks
        // are complete. I.e. so after 10 seconds
        // as per the second delay

        // The ex in this instance is the first
        // exception thrown, i.e. "A".
        var firstExceptionThrown = ex;

        // This aggregate contains both "A" and "B".
        var aggregateException = task.Exception;
    }
}

public async Task<int> A()
{
    await Task.Delay(100);
    throw new Exception("A");
}

public async Task<int> B()
{
    // Extra delay to make it clear that the await
    // waits for all tasks to complete, including
    // waiting for this exception.
    await Task.Delay(10000);
    throw new Exception("B");
}


回答6:

In your code, the first exception is returned by design as explained at http://blogs.msdn.com/b/pfxteam/archive/2011/09/28/task-exception-handling-in-net-4-5.aspx

As for your question, you will get the AggreateException if you write code like this:

try {
    var result = Task.WhenAll(DoLongThingAsyncEx1(), DoLongThingAsyncEx2()).Result; 
}
catch (Exception ex) {
    // Expect AggregateException here
} 


回答7:

This works for me

private async Task WhenAllWithExceptions(params Task[] tasks)
{
    var result = await Task.WhenAll(tasks);
    if (result.IsFaulted)
    {
                throw result.Exception;
    }
}