8

I use a set of tasks at times, and in order to make sure they are all awaited I use this approach:

public async Task ReleaseAsync(params Task[] TaskArray)
{
  var tasks = new HashSet<Task>(TaskArray);
  while (tasks.Any()) tasks.Remove(await Task.WhenAny(tasks));
}

and then call it like this:

await ReleaseAsync(task1, task2, task3);
//or
await ReleaseAsync(tasks.ToArray());

However, recently I have been noticing some strange behavior and set to see if there was a problem with the ReleaseAsync method. I managed to narrow it down to this simple demo, it runs in linqpad if you include System.Threading.Tasks. It will also work slightly modified in a console app or in an asp.net mvc controller.

async void Main()
{
 Task[] TaskArray = new Task[]{run()};
 var tasks = new HashSet<Task>(TaskArray);
 while (tasks.Any<Task>()) tasks.Remove(await Task.WhenAny(tasks));
}

public async Task<int> run()
{
 return await Task.Run(() => {
  Console.WriteLine("started");
  throw new Exception("broke");
  Console.WriteLine("complete");
  return 5;
 });
}

What I don't understand is why the Exception never shows up anywhere. I would have figured that if the Tasks with the exception were awaited, it would throw. I was able to confirm this by replacing the while loop with a simple for each like this:

foreach( var task in TaskArray )
{
  await task;//this will throw the exception properly
}

My question is, why doesn't the shown example throw the exception properly (it never shows up anywhere).

Yuval Itzchakov
  • 146,575
  • 32
  • 257
  • 321
Travis J
  • 81,153
  • 41
  • 202
  • 273
  • possible duplicate of [How to handle Task.Factory.StartNew exception?](http://stackoverflow.com/questions/22856052/how-to-handle-task-factory-startnew-exception) – David L May 20 '14 at 18:10
  • Any reason for not using `Task.WhenAll`? – Paulo Morgado May 20 '14 at 23:15
  • @PauloMorgado - Yes, these were tied to managed resources and I wanted to release them when they became available instead of waiting for all of them to complete and then releasing. – Travis J May 20 '14 at 23:16
  • This resource release is not on the code you posted, right? And managed resources "release themselves". – Paulo Morgado May 21 '14 at 06:03
  • @PauloMorgado - The release is not in the code, correct. As for the managed resource note, sorry for the typo, it was supposed to read "unmanaged" as its lifetime is dictated by a using block and inherits IDisposable. – Travis J May 21 '14 at 06:10

3 Answers3

11

TL;DR: run() throws the exception, but you're awaiting WhenAny(), which doesn't throw an exception itself.


The MSDN documentation for WhenAny states:

The returned task will complete when any of the supplied tasks has completed. The returned task will always end in the RanToCompletion state with its Result set to the first task to complete. This is true even if the first task to complete ended in the Canceled or Faulted state.

Essentially what is happening is that the task returned by WhenAny simply swallows the faulted task. It only cares about the fact that the task is finished, not that it has successfully completed. When you await the task, it simply completes without error, because it is the internal task that has faulted, and not the one you're awaiting.

Kendall Frey
  • 43,130
  • 20
  • 110
  • 148
10

A Task not being awaited or not using its Wait() or Result() method, will swallow the exception by default. This behavior can be modified back to the way it was done in .NET 4.0 by crashing the running process once the Task was GC'd. You can set it in your app.config as follows:

<configuration> 
    <runtime> 
        <ThrowUnobservedTaskExceptions enabled="true"/> 
    </runtime> 
</configuration>

A quote from this blog post by the Parallel Programming team in Microsoft:

Those of you familiar with Tasks in .NET 4 will know that the TPL has the notion of “unobserved” exceptions. This is a compromise between two competing design goals in TPL: to support marshaling unhandled exceptions from the asynchronous operation to the code that consumes its completion/output, and to follow standard .NET exception escalation policies for exceptions not handled by the application’s code. Ever since .NET 2.0, exceptions that go unhandled on newly created threads, in ThreadPool work items, and the like all result in the default exception escalation behavior, which is for the process to crash. This is typically desirable, as exceptions indicate something has gone wrong, and crashing helps developers to immediately identify that the application has entered an unreliable state. Ideally, tasks would follow this same behavior. However, tasks are used to represent asynchronous operations with which code later joins, and if those asynchronous operations incur exceptions, those exceptions should be marshaled over to where the joining code is running and consuming the results of the asynchronous operation. That inherently means that TPL needs to backstop these exceptions and hold on to them until such time that they can be thrown again when the consuming code accesses the task. Since that prevents the default escalation policy, .NET 4 applied the notion of “unobserved” exceptions to complement the notion of “unhandled” exceptions. An “unobserved” exception is one that’s stored into the task but then never looked at in any way by the consuming code. There are many ways of observing the exception, including Wait()’ing on the Task, accessing a Task’s Result, looking at the Task’s Exception property, and so on. If code never observes a Task’s exception, then when the Task goes away, the TaskScheduler.UnobservedTaskException gets raised, giving the application one more opportunity to “observe” the exception. And if the exception still remains unobserved, the exception escalation policy is then enabled by the exception going unhandled on the finalizer thread.

Yuval Itzchakov
  • 146,575
  • 32
  • 257
  • 321
  • I believe Yuval's answer explains the behavior you are seeing. In addition the exception should be accessible in the task object that run() returns. – Naylor May 20 '14 at 18:15
4

From the comment:

these [tasks] were tied to managed resources and I wanted to release them when they became available instead of waiting for all of them to complete and then releasing.

Using a helper async void method may give you the desired behavior for both removing the finished tasks from the list and immediately throwing unobserved exceptions:

public static class TaskExt
{
    public static async void Observe<TResult>(Task<TResult> task)
    {
        await task;
    }

    public static async Task<TResult> WithObservation(Task<TResult> task)
    {
        try
        {
            return await task;
        }
        catch (Exception ex)
        {
            // Handle ex
            // ...

            // Or, observe and re-throw
            task.Observe(); // do this if you want to throw immediately

            throw;
        }
    }
}

Then your code might look like this (untested):

async void Main()
{
    Task[] TaskArray = new Task[] { run().WithObservation() };
    var tasks = new HashSet<Task>(TaskArray);
    while (tasks.Any<Task>()) tasks.Remove(await Task.WhenAny(tasks));
}

.Observe() will re-throw the task's exception immediately "out-of-band", using SynchronizationContext.Post if the calling thread has a synchronization context, or using ThreadPool.QueueUserWorkItem otherwise. You can handle such "out-of-band" exceptions with AppDomain.CurrentDomain.UnhandledException).

I described this in more details here:

TAP global exception handler

Community
  • 1
  • 1
noseratio
  • 59,932
  • 34
  • 208
  • 486