At the interview they asked what would happen in this situation if both Task throw an exception and how, in this case, should they be processed?

 Task task1 = Method1Async(); Task task2 = Method2Async(); await task1; await task2; 
  • one
    It depends on the .NET version and on the settings in the config file. On the current version of .NET, the exception from the 2nd task is swallowed. - VladD
  • @VladD, exception from the 2nd task, or the second exception? - Mirdin
  • one
    @Mirdin: In the vehicle, in condition both tasks are thrown an exception. In the code as it is, await task1 will end with an exception and the code will not reach await task2 - VladD
  • one
    The question "how to handle" does not make sense. Counter-question - what should the application do if two exceptions arrive? - VladD
  • one
    @VladD Task.WhenAll() throw only one exception. If you need information on all exceptions, this is not an option. - andreycha

3 answers 3

First a bit of theory. As you know, async/await is essentially just syntactic sugar, behind the scenes, the compiler deploys asynchronous code into a bit more complicated. What happens when an exception occurs in a method marked with the async modifier? It is not immediately thrown up the stack (because by that time we can already be in a completely different place), but is placed inside the task (see the Exception property). The method itself ends normally. The exception (original or wrapped in AggregateException ) will be thrown in several cases:

  • Someone is waiting for a task (using await or the Wait() method).
  • Someone is accessing the result (the Result property, applicable only for Task<T> ).
  • If the task is unobserved (i.e. nobody has waited for it and has not turned to the result), then when the garbage collector gets to the task and calls its finalizer, the finalizer sees that there is an exception in the task and throws it out. As a result, the process falls. Important note: this behavior was enabled by default before .NET 4.5. Starting in .NET 4.5, the finalizer does not throw an exception, however, the previous behavior can be returned using the ThrowUnobservedTaskExceptions setting.

Based on this, the answer to the question is somewhat “branchist”:

Option 1. The code works under .NET under 4.5, or the ThrowUnobservedTaskExceptions setting is ThrowUnobservedTaskExceptions .

In this case, the exception for the first task will be thrown, the exception of the second task will remain unobserved. If somewhere above the stack, the exception from the first task was processed, and the application continued to work, then it will fall too late, when the finalizer of the second task throws an exception.

Option 2. The code works under .NET 4.5 and higher and the ThrowUnobservedTaskExceptions setting ThrowUnobservedTaskExceptions disabled.

In this case, the exception for the first task will still be thrown, the exception of the second task will remain unobserved (you can only see it by subscribing to the TaskScheduler.UnobservedTaskException event), but the process will not fall.

There are several options for exception handling:

  1. Wrap each await into a separate try/catch . The advantage of this option is that you will receive and handle both exceptions.
  2. Use await Task.WhenAll(task1, task2) . The disadvantage of this option is that you only receive information about the first exception.
  3. Use Task.WaitAll(task1, task2) . The advantage of this option is that you get an AggregateException that will contain both exceptions. The disadvantage of this option is that it blocks the current thread.
  • one
    Thank you, very good answer! - Lightness
  • @Lightness is nothing! - andreycha

The question is taken from here . The short answer is that the process will drop to 4.5, and after that it will not.

  • I think the author is interested in the details and not all the dock reread) - pavel
  • You will not please everyone. If you don’t reread the docks, you won’t understand where your legs grow from. And for simplifications, they will immediately lead to cons, checked. - Arheus
  • 2
    "and after - no" - only if the ThrowUnobservedTaskExceptions setting is not enabled. - andreycha

A small addition to the answer @andreycha (the first variant of processing): if you want to get all the exceptions at AggregateException , but do not use the blocking Task.WaitAll , you can use this bike:

 static async Task WhenAllWithAllExceptions(IEnumerable<Task> tasks) { var materialTasks = tasks.ToList(); // пробегаем список сейчас List<Exception> exceptions = null; bool hasExceptions = false; foreach (var task in materialTasks) { try { await task.ConfigureAwait(false); } catch (Exception ex) { if (exceptions == null) exceptions = new List<Exception>(materialTasks.Count); exceptions.Add(ex); hasExceptions = true; } } if (hasExceptions) throw new AggregateException(exceptions); } static async Task<T[]> WhenAllWithAllExceptions<T>(IEnumerable<Task<T>> tasks) { var materialTasks = tasks.ToList(); // пробегаем список сейчас T[] results = null; List<Exception> exceptions = null; bool hasExceptions = false; int taskIndex = -1; foreach (var task in materialTasks) { taskIndex++; try { T t = await task.ConfigureAwait(false); if (!hasExceptions) { if (results == null) results = new T[materialTasks.Count]; results[taskIndex] = t; } } catch (Exception ex) { if (exceptions == null) exceptions = new List<Exception>(materialTasks.Count); exceptions.Add(ex); hasExceptions = true; } } if (hasExceptions) throw new AggregateException(exceptions); return results; } 

Update: following the advice of @Pavel Mayorov, added ConfigureAwait .

  • one
    Well, yes, this is option 1 of my answer. As I already mentioned, not without a minus: one of the advantages of asynchrony is lost. PS By the way, the hasExceptions flag hasExceptions optional - they can be the fact of initialization / presence of exceptions in the exceptions list. And the method can be done extension-method. - andreycha
  • @andreycha: I still do not understand what the disadvantage is. Are the tasks “hot” anyway? As for hasExceptions , it's formally superfluous, but it seemed to me that it adds readability (which in my opinion is more important than anything else). - VladD
  • Oh, yes, stupid. You are right about the tasks. Updated my answer at the same time. - andreycha
  • @andreycha: I would put a plus, but I can not: already delivered. - VladD 2:47 pm
  • 2
    The case when ConfigureAwait(false) to place. - Pavel Mayorov 4:27 pm