27

Let's say I have three tasks, a, b, and c. All three are guaranteed to throw an exception at a random time between 1 and 5 seconds. I then write the following code:

await Task.WhenAny(a, b, c);

This will ultimately throw an exception from whichever task faults first. Since there's no try...catch here, this exception will bubble up to some other place in my code.

What happens when the remaining two tasks throw an exception? Aren't these unobserved exceptions, which will cause the entire process to be killed? Does that mean that the only way to use WhenAny is inside of a try...catch block, and then somehow observe the remaining two tasks before continuing on?

Follow-up: I'd like the answer to apply both to .NET 4.5 and .NET 4.0 with the Async Targeting Pack (though clearly using TaskEx.WhenAny in that case).

David Pfeffer
  • 38,869
  • 30
  • 127
  • 202

2 Answers2

26

What happens when the remaining two tasks throw an exception?

Those Tasks will complete in a faulted state.

Aren't these unobserved exceptions, which will cause the entire process to be killed?

Not anymore.

In .NET 4.0, the Task destructor would pass its unobserved exception to TaskScheduler.UnobservedTaskException, which would terminate the process if unhandled.

In .NET 4.5, this behavior was changed. Now, unobserved exceptions get passed to TaskScheduler.UnobservedTaskException, but then they are ignored if unhandled.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • On .NET 4.0 this doesn't terminate the process in the finalizer either though, and I don't know why. – David Pfeffer Oct 01 '12 at 18:12
  • 2
    If you installed .NET 4.5, you're running on .NET 4.5 even if you target .NET 4.0. – Stephen Cleary Oct 01 '12 at 18:15
  • So the sample code really does fail on .NET 4.0? Any way to reproduce that behavior on my (4.5) dev computer? – David Pfeffer Oct 01 '12 at 18:19
  • 2
    I *believe* the Async Targeting Pack will actually add an `UnobservedTaskException` handler that prevents process termination (I have not tested this, but that was the behavior of the Async CTP for .NET 4.0), so anything using `WhenAny` will behave the same. There is [an app config setting](http://blogs.msdn.com/b/pfxteam/archive/2011/09/28/10217876.aspx) to make .NET 4.5 emulate the old .NET 4.0 behavior. – Stephen Cleary Oct 01 '12 at 18:27
  • So with NET45 do I have to check the task state explicitly after returning from WaitAny? That's pretty annoying. – KFL Dec 14 '17 at 22:28
  • And the [post](https://blogs.msdn.microsoft.com/pfxteam/2011/09/28/task-exception-handling-in-net-4-5/#comment-158265) you referred to doesn't seem to explain why `WhenAny` doesn't propagate the exception. That said I still feel `WhenAny`'s exception handling counter-intuitive - if `WhenAny` throws, it observes the exception. If it does not throw, the exception is not "observed". It feels in every sense that the design should be to throw the exception. – KFL Dec 15 '17 at 00:47
  • @KFL: My answer doesn't talk about `WhenAny` explicitly; rather, it answers the question of "what happens to unobserved task exceptions", which is what the op was actually asking (using `WhenAny` as an example). The result of `WhenAny` is just the first task that completes; you can observe that task (or any of the others) whenever you want. – Stephen Cleary Dec 15 '17 at 00:52
  • @StephenCleary thanks for clarifying! The post you referred is a really good read! Actually I was here baffled by the fact that `WaitAny` doesn't propagate exception even if the first task faulted. And I'm struggling to find an alternative API that does throw in such case. – KFL Dec 15 '17 at 01:01
  • 3
    @KFL: `await Task.WaitAny(..)` gets you the first task to complete; to observe that task, you can use a second await: `await await Task.WhenAny(..)` – Stephen Cleary Dec 15 '17 at 01:15
  • As of .NET 6 (year 2023), I found that `WhenAny` doesn't throw an inner exception, which means that the programmer should use IsFaulted/Exception properties on the task object to handle exceptions properly. – robbie fan May 06 '23 at 03:02
  • 1
    @robbiefan: `WhenAny` returns a task whose result is the task that completed first. To observe the inner exception, `await` the inner task. E.g., `await await Task.WhenAny(..)` – Stephen Cleary May 08 '23 at 12:18
5

Yes, the remaining task exceptions are unobserved. Pre .NET 4.5 you are obliged to observe them (not sure how the situation is on .NET 4.5, but it changed).

I usually write myself a helper method for fire-and-forget tasks like these:

    public static void IgnoreUnobservedExceptions(this Task task)
    {
        if (task.IsCompleted)
        {
            if (task.IsFaulted)
            {
                var dummy = task.Exception;
            }
            return;
        }

        task.ContinueWith(t =>
            {
                var dummy = t.Exception;
            }, TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously);
    }

You might want to include logging in production apps.

usr
  • 168,620
  • 35
  • 240
  • 369