Why is this exception not thrown?

I use a set of tasks at times, and in order to make sure everyone is waiting for them, 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, I recently noticed some strange behavior and decided to check if there was a problem with the ReleaseAsync method. I managed to narrow it down to this simple demo, it works on linqpad if you enabled System.Threading.Tasks . It will also be slightly modified in the console application or in the asp.net mpc 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; }); } 

I don’t understand why Exception never appears anywhere. I would think that if expectations are expected, it will be thrown. I was able to confirm this by replacing the while loop with a simple one for everyone, like this:

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

My question is why the given example does not correctly throw an exception (it never appears anywhere).

+8
c # exception async-await task
source share
3 answers

TL; DR : run() throws an exception, but you expect WhenAny() , which does not throw an exception.


The MSDN documentation for WhenAny states:

The returned task will be completed after completing any of the assigned tasks. The returned task will always end in the RanToCompletion state, and its result will be set to the first task. This is true even if the first task completed in the Canceled or Failed state.

Essentially, what happens is that the task returned by WhenAny simply swallows the failed task. He only cares that the task is completed, not completed. When you wait for this task, it simply ends without errors, because it is an internal task that is crashing, and not the one you are expecting.

+9
source share

A Task not being awaited or using its Wait() or Result() method, will by default swallow an exception. This behavior can be changed before it was done in .NET 4.0, the workflow crashed after Task was GC'd. You can install it in app.config as follows:

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

Quote from this blog post by the Parallel Programming team at Microsoft:

Those familiar with tasks in .NET 4 will know that TPL has the concept of “invisible” exceptions. This is a trade-off between two competing design goals in TPL: support for marshaling unhandled exceptions from asynchronous operation to code that consumes its completion / output, and following the standard .NET exception escalation rules for exceptions not handled by application code. Since .NET 2.0, exceptions that are not handled in newly created threads, in ThreadPool work items, etc., All this leads to the default exception escalation behavior, which is associated with a process failure. This is usually desirable, since exceptions indicate that something went wrong, and a failure helps developers immediately determine that the application has entered an unreliable state. Ideally, tasks will follow the same behavior. However, tasks are used to represent the asynchronous operations that the code subsequently connects to, and if these asynchronous operations carry exceptions, these exceptions must be marshaled before the connection code is executed, and consuming the results of the asynchronous operation. This inherently means that the TPL must support these exceptions and hold them until they are discarded again when the consumer code addresses the task. Since this prevents the default escalation policy, .NET 4 applied the concept of "unobserved" exceptions to the concept of "unhandled" exceptions. An “invisible” exception is one that is stored in the task, but is then never treated in any way with the consumption code. There are many exceptions, including Wait () for the task, access to the result of the task, viewing the "Task Exclusions" property, etc. If the code never notices a "Tasks" exception, then when the task finishes, a TaskScheduler.UnobservedTaskException is thrown, which gives the application one more opportunity to "observe" the exception. And if the exception is still unobservable, the escalation policy of the exception is then included by the exception, which will be unhandled in the finalizer thread.

+10
source share

From the comment:

these [tasks] were tied to managed resources, and I wanted to free them when they became available, and did not wait until all of them were completed and then released.

Using the async void helper method can give you the desired behavior both to remove ready-made tasks from the list and to immediately throw invisible 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 exception immediately out of range using SynchronizationContext.Post if the calling thread has a synchronization context or uses ThreadPool.QueueUserWorkItem otherwise. You can handle such out-of-band exceptions with AppDomain.CurrentDomain.UnhandledException ).

I described it in more detail here:

TAP Global Exception Handler

+4
source share

All Articles