-
Notifications
You must be signed in to change notification settings - Fork 129
Unobserved Exceptions
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 if no catch block from all callers (call stack) will handle that exception. This approach works well for synchronous code, but not for asynchronous code. Therefore, exceptions thrown in Tasks that are not handled within the Task should not crash the process because the joining code may be responsible for handling them.
// This exception will be handled as an unobserved exception -> App will continue to run.
private void ExeceptionButtonClick(object sender, RoutedEventArgs e)
{
Task.Run(() => { throw new InvalidOperationException(); });
}
// By using await here it is considered as an unhandled exception -> App will crash.
private async void ExeceptionButton2Click(object sender, RoutedEventArgs e)
{
await Task.Run(() => { throw new InvalidOperationException(); });
}
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 several 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 no longer used (referenced), the GarbageCollector calls the finalizer of the Task object. The finalizer raises the TaskScheduler.UnobservedTaskException
event, giving the application one last chance to observe the exception. This event can also be used to log unobserved exceptions.
The sample code shows that unobserved exceptions occur only when await
is not involved. Otherwise, the exception is treated as an unhandled exception that will crash the process.
By using await
, the code here behaves similarly to synchronous code.
If the exception still remains unobserved then the .NET runtime behaves as follows:
- The unobserved exception is eaten up without further action.
If the exception still remains unobserved then the .NET Framework runtime behaves as follows:
- .NET Framework 4: The process crashes. This is similar to the behavior for unhandled exceptions.
- .NET Framework 4.5 or later: The unobserved exception is eaten up without further action.
I think the new behavior is dangerous because this way a developer might miss serious bugs 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>