microsoft / microsoft/microsoft-ui-xaml
Exceptions are inconsistently handled depending on their source
- Dominant language
- C++
- Stars
- 8.4k
- Forks
- 942
- Avg merge
- 2d 7h
- Merged PRs (30d)
- 105
Description
### Describe the bug
Depending on where and how an exception is thrown, any of the following may occur: nothing, a crash, an unhandled exception event, or an unhandled exception event and a crash.
### Why is this important?
It is vital for reliability that an unhandled exception **_always_** results in a fail-fast as soon as possible. As exceptions can occur at virtually any point in the program's flow of execution, application state may not be well-formed at the point where an unexpected exception is thrown. In general, it is impossible for the app's state to be correctly cleaned up in this case, as the handler has no possible way of knowing what to do.
This creates significant reliability problems, as corrupt state is inevitable when this happens. This will result in data loss, unexpected app behavior, unseen violations of invariants, etc. It also creates significant security risks, as an app in an undetermined state is in a state it was not prepared for, which can potentially be exploited.
Additionally, this behavior must be consistent _regardless of how or where the exception is thrown_, and this behavior must be consistent with standard .NET practices and expectations in the C# projections. Developers cannot be expected to know, understand, and anticipate behavioral differences caused by how their code is consumed.
### Steps to reproduce the bug
Use the template in Visual Studio to create a new C# WinUI app. When the `Application` object is created, hook up the two possible unhandled event handlers:
```
this.UnhandledException += App_UnhandledException;
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
```
Define some buttons in the xaml for MainWindow
```
Sync
Thread pool
Async void UI thread
Async void worker thread
Dispatcher queue
Dispatcher queue sync context
```
and implement these methods
```
private void TestSnyc(object sender, RoutedEventArgs e)
{
Fail();
}
private async void TestAsyncVoidUI(object sender, RoutedEventArgs e)
{
await Task.Yield();
Fail();
}
private async void TestAsyncVoidThread(object sender, RoutedEventArgs e)
{
await Task.Run(async () => { Fail(); });
return;
}
private void TestDispatcherQueue(object sender, RoutedEventArgs e)
{
this.DispatcherQueue.TryEnqueue(() => Fail());
}
private async void TestDispatcherQueueSyncContext(object sender, RoutedEventArgs e)
{
var timer = DispatcherQueue.CreateTimer();
timer.Tick += (sender, e) => { Fail(); };
timer.IsRepeating = false;
timer.Interval = TimeSpan.FromSeconds(1);
timer.Start();
}
private async void TestThreadPool(object sender, RoutedEventArgs e)
{
ThreadPool.QueueUserWorkItem((_) => Fail());
}
private void Fail([CallerMemberName] string? name = null)
{
throw new Exception($"Failing {name}");
}
```
Then put a breakpoint in the two exception handler callbacks, and then run the app and observe that the behavior is different.
### Actual behavior
`TestSnyc`: fires `App_UnhandledException`, then crashes
`TestThreadPool`: fires `CurrentDomain_UnhandledException`, then some time later on crashes
`TestAsyncVoidUI`: fires `App_UnhandledException`, no crash
`TestAsyncVoidThread`: fires `App_UnhandledException`, no crash
`TestDispatcherQueue`: No event fires, continues if a debugger is attached, otherwise crashes
`TestDispatcherQueueSyncContext`: No event fires, continues if a debugger is attached, otherwise crashes
### Expected behavior
In each of these cases, the behavior must be identical:
1. fires `App_UnhandledException`
2. if the exception is not handled, fires `CurrentDomain_UnhandledException`
3. if the exception is still not handled, immediately fail fast
The fail fast should happen as early as possible, to preserve as much context in any dump as possible, as well as to prevent any opportunities for subsequently executed state to corrupt persisted state (eg the user's document) or to for any exploits to be used.
### Screenshots
_No response_
### NuGet package version
WinUI 3 - Windows App SDK 1.8.3: 1.8.251106002
### Windows version
Windows Insider Build (xxxxx)
### Additional context
Keep in mind that the .NET behavior for a synchronously thrown unhandled exception is that, unlike in C++, before the stack is unwound it is walked to determine handlers. If no handlers are found, a dump is taken at the point where the exception was thrown, and only then is the stack unwound and finally blocks executed. This results in relatively high-quality and easy to understand Watson reports, as the immediate context of the error is captured.
Contributor guide
Research direction
Start with the C# WinUI template and reproduce each listed path: synchronous, thread-pool, async-void, and DispatcherQueue exceptions, observing both unhandled-exception callbacks. The change is complete when every path consistently invokes App_UnhandledException, then CurrentDomain_UnhandledException when unhandled, and finally fails fast immediately; the issue names no repository files or tests to edit.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp, csharp
- Domain
- desktop
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 25/100