171

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?

Govert
  • 16,387
  • 4
  • 60
  • 70
Michael Ray Lovett
  • 6,668
  • 7
  • 27
  • 36
  • 11
    WhenAll *does* create an `AggregateException`. If you used `Task.Wait` instead of `await` in your example, you'd catch `AggregateException` – Peter Ritchie Aug 17 '12 at 15:55
  • 2
    +1, this is what I am trying to figure out, save me hours of debugging and google-ing. – kennyzx Oct 23 '12 at 16:54
  • For the first time in quite a few years I needed all exceptions from `Task.WhenAll`, and I fell into the same trap. So I've tried [going into deep details](https://stackoverflow.com/a/62607500/1768303) about this behavior. – noseratio Jun 27 '20 at 11:30
  • 1
    Related: [I want await to throw AggregateException, not just the first Exception](https://stackoverflow.com/questions/18314961/i-want-await-to-throw-aggregateexception-not-just-the-first-exception) – Theodor Zoulias Dec 26 '20 at 22:57
  • @PeterRitchie This is true, but be aware that `Task.Wait` is blocking, `await` is not. – xr280xr Mar 27 '21 at 05:05

8 Answers8

130

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.

Richiban
  • 5,569
  • 3
  • 30
  • 42
  • 5
    +1, but can't you simply put the `throw task.Exception;` inside the `catch` block? (It confuses me to see an empty catch when exceptions are actually being handled.) – AnorZaken Nov 06 '19 at 13:32
  • @AnorZaken Absolutely; I don't remember why I wrote it like that originally, but I can't see any downside so I've moved it in the catch block. Thanks – Richiban Dec 23 '19 at 14:22
  • 1
    One minor downside of this approach is the cancellation status (`Task.IsCanceled`) doesn't get properly propagated. This can be solves using an extension helper like [this](https://stackoverflow.com/a/62607500/1768303). – noseratio Jun 28 '20 at 22:09
  • 2
    I might be misreading something, but how can you do var results =await task; when Task.WhenAll() returns Task, so awaiting it returns void? – David Jacobsen Jul 28 '21 at 15:37
  • 3
    @DavidJacobsen It depends on the types of the tasks that you pass in; since in this case `A` and `B` both return `Task` this works (`Task.WhenAll()` will return `Task`). If `A` and `B` returned different types or at least one of them was `void` then you'd be correct and `var results = await task` wouldn't work. – Richiban Jul 28 '21 at 17:16
  • 1
    Getting error on var results = await task; "Awaited task returns no value" – Amit Verma Feb 08 '23 at 05:17
97

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....

Community
  • 1
  • 1
decyclone
  • 30,394
  • 6
  • 63
  • 80
  • 5
    Yeah, I know there's been some changes to exception handling, but the newest docs for Task.WhenAll state "If any of the supplied tasks completes in a faulted state, the returned task will also complete in a Faulted state, where its exceptions will contain the aggregation of the set of unwrapped exceptions from each of the supplied tasks".... In my case, both of my tasks are completing in a faulted state... – Michael Ray Lovett Aug 17 '12 at 14:41
  • 5
    @MichaelRayLovett: You are not storing the returned Task anywhere. I bet when you look at the Exception property of that task, you would get an AggregateException. But, in your code, you are using await. That makes the AggregateException to be unwrapped into the actual exception. – decyclone Aug 17 '12 at 14:45
  • 3
    I thought of that, too, but two problems came up: 1) I can't seem to figure out how to store the task so I can examine it (ie "Task myTask = await Task.WhenAll(...)" doesn't seem to work. and 2) I guess I don't see how await could ever represent multiple exceptions as just one exception.. which exception should it report? Pick one at random? – Michael Ray Lovett Aug 17 '12 at 14:52
  • Task myTask = Task.WhenAll(...); await myTask; – decyclone Aug 17 '12 at 14:54
  • 2
    Yes, when I store the task and examine it in the try/catch of the await, I see it's exception is AggregatedException. So the docs I read are right; Task.WhenAll is wrapping up the exceptions in an AggregateException. But then await is unwrapping them. I'm reading your article now, but I don't see yet how await can pick a single exception from the AggregateExceptions and throw that one vs. another one.. – Michael Ray Lovett Aug 17 '12 at 15:02
  • 3
    Read the article, thanks. But I still don't get why await represents an AggregateException (representing multiple exceptions) as just one single exception. How is that a comprehensive handling of exceptions? .. I guess if I want to know exactly which tasks threw exceptions and which ones they threw, I would have to examine the Task object created by Task.WhenAll?? – Michael Ray Lovett Aug 17 '12 at 15:29
  • 2
    Yes, you will need to examine the returned Task. And just to put it as simply as I can, in code that does not use async/await, how many exceptions do you handle at once? The answer is probably one. It's usually the last exception that was thrown and not handled. With await, they seem to be creating the exact same scenario. If you want advanced control, you can always examine the Exception property of the returned task. – decyclone Aug 17 '12 at 15:36
  • 2
    Thanks, I got it now. This article spells things out pretty clearly: http://blogs.msdn.com/b/pfxteam/archive/2011/09/28/10217876.aspx – Michael Ray Lovett Aug 17 '12 at 16:27
  • I think that this is a good answer when an `await` is used for a single `async` task. However, in the case where `WhenAll` is used, it seems better to get the aggregated exceptions for each failed task, which isn't possible when using `await` with `WhenAll` (in .NET Core 3.1), unless you catch the exception thrown by `await` and then check the `Exception` attribute of the task returned from `WhenAll`. – Toby Artisan Mar 02 '20 at 16:27
  • What happens when you have multiple exceptions thrown in multiple tasks inside of a Task.WhenAll? How could it throw anything but an AggregateException? If it didn't how would it know which exception to throw? Just the first one? – rollsch Jun 16 '20 at 06:17
  • 1
    @rolls, check [my answer](https://stackoverflow.com/a/62607500/1768303) which covers this and some other subtleties. – noseratio Jun 28 '20 at 22:04
  • [Here's](http://docs.google.com/viewer?url=http://download.microsoft.com/download/5/4/B/54B83DFE-D7AA-4155-9687-B0CF58FF65D7/async-primer.pdf) the PDF readable in your browser. – c24w Jul 23 '20 at 10:27
49

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(); });
}
themefield
  • 3,847
  • 30
  • 32
jgauffin
  • 99,844
  • 45
  • 235
  • 372
  • 18
    The previous two comments are incorrect. The code does in fact work and `exceptions` contains both exceptions thrown. – Tobias May 10 '18 at 23:52
  • DoLongThingAsyncEx2() must throw new InvalidOperationException() instead of new InvalidOperation() – Artemious Dec 08 '18 at 22:53
  • 11
    To alleviate any doubt here, I put together an extended fiddle that hopefully shows exactly how this handling plays out: https://dotnetfiddle.net/X2AOvM. You can see that the `await` causes the first exception to be unwrapped, but all exceptions are indeed still available via the array of Tasks. – nuclearpidgeon Jan 02 '19 at 01:18
  • @nuclearpidgeon While your fiddle, and OP's solution both work, they require you to keep track of the tasks, and ignore the whole aggregating use of the `AggregateException`. `allTasksCompleted.Exception.InnerException` holds the 'first' exception, the same one caught after `await` fails. But then, iterate through `allTasksCompleted.Exception.InnerException*s*` to iterate through the multiple exceptions, or use `.Flatten()` to recursively turn any aggregations into one enumerable. – Wolfzoon Sep 15 '21 at 11:27
49

Many good answers here, but I still would like to post my rant as I've just come across the same problem and conducted some research. Or skip to the TLDR version below.

The problem

Awaiting the task returned by Task.WhenAll only throws the first exception of the AggregateException stored in task.Exception, even when multiple tasks have faulted.

The current docs for Task.WhenAll say:

If any of the supplied tasks completes in a faulted state, the returned task will also complete in a Faulted state, where its exceptions will contain the aggregation of the set of unwrapped exceptions from each of the supplied tasks.

Which is correct, but it doesn't says anything about the aforementioned "unwrapping" behavior of when the returned task is awaited.

I suppose, the docs don't mention it because that behavior is not specific to Task.WhenAll.

It is simply that Task.Exception is of type AggregateException and for await continuations it always gets unwrapped as its first inner exception, by design. This is great for most cases, because usually Task.Exception consists of only one inner exception. But consider this code:

Task WhenAllWrong()
{
    var tcs = new TaskCompletionSource<DBNull>();
    tcs.TrySetException(new Exception[]
    {
        new InvalidOperationException(),
        new DivideByZeroException()
    });
    return tcs.Task;
}

var task = WhenAllWrong();    
try
{
    await task;
}
catch (Exception exception)
{
    // task.Exception is an AggregateException with 2 inner exception 
    Assert.IsTrue(task.Exception.InnerExceptions.Count == 2);
    Assert.IsInstanceOfType(task.Exception.InnerExceptions[0], typeof(InvalidOperationException));
    Assert.IsInstanceOfType(task.Exception.InnerExceptions[1], typeof(DivideByZeroException));

    // However, the exception that we caught here is 
    // the first exception from the above InnerExceptions list:
    Assert.IsInstanceOfType(exception, typeof(InvalidOperationException));
    Assert.AreSame(exception, task.Exception.InnerExceptions[0]);
}

Here, an instance of AggregateException gets unwrapped to its first inner exception InvalidOperationException in exactly the same way as we might have had it with Task.WhenAll. We could have failed to observe DivideByZeroException if we did not go through task.Exception.InnerExceptions directly.

Microsoft's Stephen Toub explains the reason behind this behavior in the related GitHub issue:

The point I was trying to make is that it was discussed in depth, years ago, when these were originally added. We originally did what you're suggesting, with the Task returned from WhenAll containing a single AggregateException that contained all the exceptions, i.e. task.Exception would return an AggregateException wrapper which contained another AggregateException which then contained the actual exceptions; then when it was awaited, the inner AggregateException would be propagated. The strong feedback we received that caused us to change the design was that a) the vast majority of such cases had fairly homogenous exceptions, such that propagating all in an aggregate wasn't that important, b) propagating the aggregate then broke expectations around catches for the specific exception types, and c) for cases where someone did want the aggregate, they could do so explicitly with the two lines like I wrote. We also had extensive discussions about what the behavior of await sould be with regards to tasks containing multiple exceptions, and this is where we landed.

One other important thing to note, this unwrapping behavior is shallow. I.e., it will only unwrap the first exception from AggregateException.InnerExceptions and leave it there, even if it happens to be an instance of another AggregateException. This may add yet another layer of confusion. For example, let's change WhenAllWrong like this:

async Task WhenAllWrong()
{
    await Task.FromException(new AggregateException(
        new InvalidOperationException(),
        new DivideByZeroException()));
}

var task = WhenAllWrong();

try
{
    await task;
}
catch (Exception exception)
{
    // now, task.Exception is an AggregateException with 1 inner exception, 
    // which is itself an instance of AggregateException
    Assert.IsTrue(task.Exception.InnerExceptions.Count == 1);
    Assert.IsInstanceOfType(task.Exception.InnerExceptions[0], typeof(AggregateException));

    // And now the exception that we caught here is that inner AggregateException, 
    // which is also the same object we have thrown from WhenAllWrong:
    var aggregate = exception as AggregateException;
    Assert.IsNotNull(aggregate);
    Assert.AreSame(exception, task.Exception.InnerExceptions[0]);
    Assert.IsInstanceOfType(aggregate.InnerExceptions[0], typeof(InvalidOperationException));
    Assert.IsInstanceOfType(aggregate.InnerExceptions[1], typeof(DivideByZeroException));
}

A solution (TLDR)

So, back to await Task.WhenAll(...), what I personally wanted is to be able to:

  • Get a single exception if only one has been thrown;
  • Get an AggregateException if more than one exception has been thrown collectively by one or more tasks;
  • Avoid having to save the Task only for checking its Task.Exception;
  • Propagate the cancellation status properly (Task.IsCanceled), as something like this would not do that: Task t = Task.WhenAll(...); try { await t; } catch { throw t.Exception; }.

I've put together the following extension for that:

public static class TaskExt 
{
    /// <summary>
    /// A workaround for getting all of AggregateException.InnerExceptions with try/await/catch
    /// </summary>
    public static Task WithAggregatedExceptions(this Task @this)
    {
        // using AggregateException.Flatten as a bonus
        return @this.ContinueWith(
            continuationFunction: anteTask =>
                anteTask.IsFaulted &&
                anteTask.Exception is AggregateException ex &&
                (ex.InnerExceptions.Count > 1 || ex.InnerException is AggregateException) ?
                Task.FromException(ex.Flatten()) : anteTask,
            cancellationToken: CancellationToken.None,
            TaskContinuationOptions.ExecuteSynchronously,
            scheduler: TaskScheduler.Default).Unwrap();
    }    
}

Now, the following works the way I want it:

try
{
    await Task.WhenAll(
        Task.FromException(new InvalidOperationException()),
        Task.FromException(new DivideByZeroException()))
        .WithAggregatedExceptions();
}
catch (OperationCanceledException) 
{
    Trace.WriteLine("Canceled");
}
catch (AggregateException exception)
{
    Trace.WriteLine("2 or more exceptions");
    // Now the exception that we caught here is an AggregateException, 
    // with two inner exceptions:
    var aggregate = exception as AggregateException;
    Assert.IsNotNull(aggregate);
    Assert.IsInstanceOfType(aggregate.InnerExceptions[0], typeof(InvalidOperationException));
    Assert.IsInstanceOfType(aggregate.InnerExceptions[1], typeof(DivideByZeroException));
}
catch (Exception exception)
{
    Trace.WriteLine($"Just a single exception: ${exception.Message}");
}
noseratio
  • 59,932
  • 34
  • 208
  • 486
  • 4
    Fantastic answer – rollsch Jun 30 '20 at 23:35
  • 1
    A tiny note regarding the `WithAggregatedExceptions` method's implementation: AFAIK the condition `anteTask.Exception is AggregateException ex` will always succeed, so it just serves for assigning the `anteTask.Exception` to the `ex` variable. – Theodor Zoulias Feb 14 '21 at 19:15
  • 1
    @TheodorZoulias, ideed! I just wanted a one-liner to introduce `ex` :) – noseratio Feb 14 '21 at 20:41
  • One more note: The idea of propagating an `AggregatedException` only in case that the task stores internally more than one exceptions, is problematic. Imagine for example that you are awaiting a `Task.WhenAll` that can fail by one or more `FileNotFoundException`. In that case you'd have to duplicate your error handling logic, having both a `catch (FileNotFoundException)` and a `catch (AggregateException aex)` that filters for `FileNotFoundException`s. And the same handling logic should be on both handlers. Personally I find simpler to propagate an `AggregatedException` in all cases. – Theodor Zoulias Feb 14 '21 at 21:28
  • Although special casing solitary exceptions makes sense in case this exception is an `OperationCanceledException`. Propagating an `AggregateException` that contains an `OperationCanceledException` is awkward. – Theodor Zoulias Feb 15 '21 at 01:55
  • @TheodorZoulias, I see you point. I'm yet to face this scenario, but in that case, a simplified version can be used (no need for `ex.InnerExceptions.Count > 1 || ex.InnerException is AggregateException` check), it will always give you an unwrapped `AggregateException`. – noseratio Feb 15 '21 at 02:16
  • In most cases though (and I agree for Stephen Tousb and Stephen Cleary here), we only care about the fact of the exception, and the first one is enough. The best we could do is to log and perhaps retry. And this is where my extension is more useful I believe. It gives the aggregated diag info while handling a generic `Exception`, example: https://try.dot.net/?bufferId=WithAggregatedExceptions.cs&fromGist=628f06c68ad0d2cb1252279eec17ea41&canshowgithubpanel=true – noseratio Feb 15 '21 at 02:16
  • And as to `OperationCanceledException`, it's not just the exception itself, but also the `Task.IsCanceled` status that gets correctly propagated by this extension. – noseratio Feb 15 '21 at 02:21
  • 1
    I don't think `Task.IsCanceled` will be correctly propagated when using `try { await source.ConfigureAwait(false); } catch { source.Wait(); }` like [this](https://github.com/dotnet/runtime/issues/47605#issuecomment-778827371). I think it it will become `Task.IsFaulted`. Not a big issue perhaps, but worth paying attention. – noseratio Feb 15 '21 at 02:31
  • 1
    Yeap, you are right that the `IsCanceled` is not preserved, which is kinda sad. AFAIK the asynchronous methods can never produce a task having (eventually) a canceled state. This makes your `ContinueWith`+`Wrap` approach superior I must say. I wish there was an attribute or something that could configure the async state machine, to make emitting canceled tasks possible. Btw have you noticed how bad is the `Message` of a flattened `AggregateException`? Each inner message is repeated twice. My workaround is to do this: `new AggregateException(ex.Flatten().InnerExceptions)` – Theodor Zoulias Feb 15 '21 at 02:59
  • @TheodorZoulias, yep the duplicate messages are bad... but better than "One or more errors occurred" :) I'll update the answer with you suggestion about `ex.Flatten().InnerExceptions`, if you don't mind. – noseratio Feb 15 '21 at 03:17
  • 2
    BTW, *AFAIK the asynchronous methods can never produce a task having (eventually) a canceled state* - not sure if I follow, but this one does: `async Task TestAsync() { await Task.FromException(new TaskCanceledException()); }`. The `Task.IsCanceled` will be `true` here, same as if we just did `throw new TaskCanceledException()` inside an `async method`. – noseratio Feb 15 '21 at 03:23
  • 1
    Yeap, you are right. I just tested it, and the `IsCanceled` is preserved. And with the correct `CancellationToken`. I had it completely wrong in my mind. Btw, yes, of course you can edit the question with that suggestion! – Theodor Zoulias Feb 15 '21 at 03:30
16

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");
}
Daniel Šmon
  • 161
  • 1
  • 3
11

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

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

Just code
  • 13,553
  • 10
  • 51
  • 93
Mohit Datta
  • 151
  • 1
  • 3
  • 3
    This is wrong, the task returned from the `WhenAll` method have an `Exception` property that is an `AggregateException` containing all the exceptions thrown in its `InnerExceptions`. What's happening here is that `await` throwing the first inner exception instead of the `AggregateException` itself (like decyclone said). Calling the task's `Wait` method instead of awaiting it causes the original exception to be thrown. – Şafak Gür Feb 21 '18 at 11:36
  • Actually this answer and the previous commenter are both accurate. The `await` on `WhenAll` will unwrap the aggregate exception and pass the first exception in the list to the catch. For the purpose of the original question to get an aggregate exception in the catch block as expected, then a `Task.WaitAll` should be used – Theo Dec 03 '20 at 16:56
1

What you really need to do is:

await Task.WhenAll(DoLongThingAsyncEx1(), DoLongThingAsyncEx2())
     .ContinueWith(t => throw t.Exception!.Flatten(), TaskContinuationOptions.OnlyOnFaulted);
Alexei Sosin
  • 2,753
  • 1
  • 10
  • 13
  • I tested your approach and it fails when no exception is thrown. It causes to the current task to be cancelled since the `.ContinueWith` was canceled due the filter `TaskContinuationOptions.OnlyOnFaulted`. I think it's better to test if the task is faulted like this to avoid canceling the current task: `.ContinueWith(t => t.IsFaulted ? throw t.Exception!.Flatten() : t);`. See on fiddle: https://dotnetfiddle.net/eL2LjS – Maico Aug 24 '23 at 14:53
-4

This works for me

private async Task WhenAllWithExceptions(params Task[] tasks)
{
    var result = await Task.WhenAll(tasks);
    if (result.IsFaulted)
    {
                throw result.Exception;
    }
}
Alexey Kulikov
  • 1,097
  • 1
  • 14
  • 38
  • 1
    `WhenAll` is not the same as `WhenAny`. `await Task.WhenAny(tasks)` will complete as soon as any task is completed. So if you have one task that completes immediately and is successful and another takes a few seconds before throwing an exception, this will return immediately without any error. – StriplingWarrior Mar 01 '19 at 00:13
  • Then throw line will never be hit here - WhenAll would have thrown the exception – thab May 20 '19 at 09:20