Skip to content

Unobserved Exceptions

jbe2277 edited this page Oct 17, 2015 · 2 revisions

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. But if you need to use a library that does not carefully catch all the exceptions then you must switch back to the default .NET 4.5 behavior.

Further readings

  1. MSDN Blog Task Exception Handling in .NET 4.5
Clone this wiki locally