22

Today my colleagues and I discussed how to handle exceptions in C# 5.0 async methods correctly, and we wondered if awaiting multiple tasks at once also observes the exceptions that do not get unwrapped by the runtime.

Consider the following code snippet:

async Task ExceptionMethodAsync()
{
    await Task.Yield();
    throw new Exception();
}

async Task CallingMethod()
{
    try
    {
        var a = ExceptionMethodAsync();
        var b = ExceptionMethodAsync();

        await Task.WhenAll(a, b);
    }
    catch(Exception ex)
    {
        // Catches the "first" exception thrown (whatever "first" means)

    }
}

What happens to the second task now? Both will be in a faulted state, but is the second task's exception now observed or unobserved?

tRuEsAtM
  • 3,517
  • 6
  • 43
  • 83
Peit
  • 882
  • 6
  • 17

1 Answers1

40

Task.WhenAll returns a task and like all tasks the Exception property holds an AggregateException that combines all exceptions.

When you await such a task only the first exception will actually be thrown.

... Whether because of child tasks that fault, or because of combinators like Task.WhenAlll, a single task may represent multiple operations, and more than one of those may fault. In such a case, and with the goal of not losing exception information (which can be important for post-mortem debugging), we want to be able to represent multiple exceptions, and thus for the wrapper type we chose AggregateException.

... Given that, and again having the choice of always throwing the first or always throwing an aggregate, for “await” we opt to always throw the first

from Task Exception Handling in .NET 4.5

It's up to you to choose if you want to handle just the first using await task; (true in most cases) or handle all using task.Exception (as in my example below), but in both cases a and b would not raise an UnobservedTaskException.

var task = Task.WhenAll(a, b);
try
{
    await task;
}
catch
{
    Trace.WriteLine(string.Join(", ", task.Exception.Flatten().InnerExceptions.Select(e => e.Message)));
}
Igor
  • 349
  • 1
  • 6
i3arnon
  • 113,022
  • 33
  • 324
  • 344
  • Should we call `task.Exception.Handle(_ => true)` on the catch or are the child exceptions marked as "handled" by using `await`? – jorgebg Oct 06 '17 at 09:07
  • 1
    @jorgebg I assume `Task.WhenAll` "handles" the exceptions of all the individual tasks. All you need to do is handle the exception of the compound task `Task.WhenAll` returns. So AFAIK, `await Task.WhenAll(...)` is enough. – i3arnon Oct 06 '17 at 10:57
  • When I need to handle each task's exception separately and know which task it comes from, I guess I could iterate all tasks I passed to `Task.WhenAll` and check their `Exception`, instead of using the combined task's `Exception`, right? – ygoe Oct 25 '18 at 10:46