📜 ⬆️ ⬇️

Handling exceptions in asynchronous code when upgrading to .NET 4.5

In the post I will try to uncover the pitfalls that arise when handling exceptions in asynchronous code in .NET 4 in the context of .NET 4.5

If you are wondering why under some conditions the code from the example below finishes work without an unhandled exception, welcome under cat.

Consider how the code from the example will behave depending on which .NET version is installed on the server:

class Program { static void Main(string[] args) { Task.Factory.StartNew(() => { throw new Exception(); }); Thread.Sleep(1000); GC.Collect(); } } 

')
If .NET 4 is installed on the server and .NET 4.5 is not installed, the process will terminate due to an unhandled exception. The generation of exceptions from tasks that no one expects to complete occurs during garbage collection. If the example would look like this:

 class Program { static void Main(string[] args) { Task.Factory.StartNew(() => { throw new Exception(); }); Thread.Sleep(1000); } } 


That problem would be hard to notice.

To handle such exceptions, the TaskScheduler type has a TaskUnobservedException event. The event allows you to catch the exception and prevent the completion of the process.

If .NET 4.5 is installed on the server, the behavior changes, the process does not end due to an unhandled exception.
If in .NET 4.5 there would be a standard behavior from .NET 4, then in the example below, when garbage collection occurs, after calling the SomeMethod method, the process would end, because the exception from Async2 () would remain unprocessed:

 public static async Task SomeMethod() { try { var t1 = Async1(); var t2 = Async2(); await t1; await t2; } catch { } } public static Task Async1() { return Task.Factory.StartNew(() => { throw new Exception(); }); } public static Task Async2() { return Task.Factory.StartNew(() => { throw new Exception(); }); } 


To install the default behavior from .NET 4 after installing .NET 4.5, you need to add the ThrowUnobservedTaskExceptions key to the application configuration file.

In practice, such a change in behavior during the transition from one version of the framework to another is dangerous in that the live may not have .NET 4.5 installed, and the developer works in a system with .NET 4.5. In this case, the developer may miss such an error. Therefore, when developing, it is strongly recommended to test the application with the ThrowUnobservedTaskExceptions key enabled .

In .NET 4.5 there is another innovation that can cause a lot of problems - async void methods, which are otherwise handled by the compiler, rather than async Task methods. AsyncVoidMethodBuilder is used for processing async void methods, and AsyncTaskMethodBuilder for async Task methods. If errors occur, if there is no synchronization context, an exception will be thrown into the thread pool, which leads to the completion of the process. Such an exception can be caught , but it is impossible to prevent the completion of the process. async void methods should only be used to handle events from UI elements.

An example of a non-obvious use of the async void method, which leads to the completion of the process:

 new List<int>().ForEach(async i => { throw new Exception(); }); 

If you do not need async void methods, you can add a rule to CI, which, when appearing in As IL using AsyncVoidMethodBuilder, would consider the build unsuccessful.

Sources:
  1. http://www.jaylee.org/post/2012/07/08/c-sharp-async-tips-and-tricks-part-2-async-void.aspx
  2. http://blogs.msdn.com/b/cellfish/archive/2012/10/18/the-tale-of-an-unobservedtaskexception.aspx
  3. http://blogs.msdn.com/b/pfxteam/archive/2011/09/28/10217876.aspx

Source: https://habr.com/ru/post/231665/


All Articles