Asynchronous exception handling when migrating to .NET 4.5
In a 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 will exit without an unhandled exception, welcome to cat.
Consider how the code from the example will behave, depending on which .NET versions are installed on the server:
If .NET 4 is installed on the server and .NET 4.5 is not installed, the process will end due to an unhandled exception. Exclusions from tasks that no one expects to complete occur during garbage collection. If the example looked like this:
That problem would be hard to notice.
To handle such exceptions in an event type TaskScheduler TaskUnobservedException . 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 the standard behavior from .NET 4 would remain, then in the example below, when garbage collection after calling SomeMethod method, the process would end, because an exception from Async2 () would remain unhandled:
To return to the standard 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 when switching from one version of the framework to another is dangerous because .NET 4.5 may not be installed on live, and the developer works on a system with .NET 4.5. In this case, the developer may miss a similar error. Therefore, during development, it is strongly recommended that you 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 that are otherwise processed by the compiler than async Task methods. For processing async void methods usedAsyncVoidMethodBuilder , and for async Task methods AsyncTaskMethodBuilder . 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. You can catch such an exception , but you won’t be able to prevent the completion of the process. async void methods should only be used to handle events from UI elements.
An example of the non-obvious use of the async void method, which leads to the completion of the process:
If you do not need async void methods, you can add a rule in CI that, when the AsyncVoidMethodBuilder appears in IL, would consider the build to be unsuccessful.
Sources:
If you are wondering why under some conditions the code from the example below will exit without an unhandled exception, welcome to cat.
Consider how the code from the example will behave, depending on which .NET versions are 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 end due to an unhandled exception. Exclusions from tasks that no one expects to complete occur during garbage collection. If the example looked 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 in an event type TaskScheduler TaskUnobservedException . 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 the standard behavior from .NET 4 would remain, then in the example below, when garbage collection after calling SomeMethod method, the process would end, because an exception from Async2 () would remain unhandled:
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 return to the standard 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 when switching from one version of the framework to another is dangerous because .NET 4.5 may not be installed on live, and the developer works on a system with .NET 4.5. In this case, the developer may miss a similar error. Therefore, during development, it is strongly recommended that you 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 that are otherwise processed by the compiler than async Task methods. For processing async void methods usedAsyncVoidMethodBuilder , and for async Task methods AsyncTaskMethodBuilder . 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. You can catch such an exception , but you won’t be able to prevent the completion of the process. async void methods should only be used to handle events from UI elements.
An example of the non-obvious use of the async void method, which leads to the completion of the process:
new List().ForEach(async i => { throw new Exception(); });
If you do not need async void methods, you can add a rule in CI that, when the AsyncVoidMethodBuilder appears in IL, would consider the build to be unsuccessful.
Sources: