[Info] Asynchronous Programming: Unobserved Exceptions

Coordinator
Apr 11, 2014 at 6:50 PM
Edited May 14, 2016 at 7:27 PM

This article has been moved to GitHub!


The default exception escalation behavior of the .NET runtime is that an unhandled exception forces the process to crash. The crash helps developers to see that something has gone wrong and the application has entered an unreliable state.

An exception is “unhandled” when no catch block from all callers (call stack) is going to handle this exception. This approach works fine for synchronous code but it does not work for asynchronous code. Therefore, exceptions thrown in Tasks which are not handled within the Task should not crash the process because the joining code might be responsible to handle it.

Example:
try
{
    await Task.Run(() => { throw new InvalidOperationException(); });
}
catch (InvalidOperationException)
{
    // The joining code handles this exception...
}
A Task comes with the concept of “unobserved” Exceptions. Exceptions that are not handled within the Task are stored and marked as unobserved. The joining code can observe the exception in various ways:
  • Explicitly: Read the Task.Exception property
  • Implicitly: Use the await keyword or call one of the blocking methods on the Task (e.g. Task.Wait())
If the code never observes a Task’s exception and the Task is not used (referenced) anymore then the GarbageCollector calls the finalizer of the Task object. The finalizer raises the TaskScheduler.UnobservedTaskException event that allows the application to observe the exception as a last chance. This event could also be used to log unobserved exceptions.

If the exception still remains unobserved then the .NET runtime behaves as following:
  • .NET 4: The process crashes. This is similar to the behavior for unhandled exceptions.
  • .NET 4.5 or newer: The unobserved exception is eaten up without further actions.
I believe the new behavior is dangerous because this way a developer might miss serious errors in the application. If the exceptions are just eaten up then you do not get any indication that something went wrong. However, the runtime provides an option to use the old crashing behavior of .NET 4.
<configuration> 
    <runtime> 
        <ThrowUnobservedTaskExceptions enabled="true"/> 
    </runtime> 
</configuration>
Microsoft recommends to activate this option (see MSDN Blog). In my opinion this should still be the default option. Always try to activate this option. There is one exception. If you need to use a library that does not carefully catch all the exceptions then you have switch back to the default behavior.

Further readings
  1. MSDN Blog Task Exception Handling in .NET 4.5