[release/9.0] Prevent unnecessary debugger stops for user-unhandled exceptions in Blazor apps with Just My Code enabled by halter73 · Pull Request #58573 · dotnet/aspnetcore (original) (raw)
Prevent unnecessary debugger stops for user-unhandled exceptions in Blazor apps with Just My Code enabled
Improve usage of the new [DebuggerDisableUserUnhandledExceptions]
and Debugger.BreakForUserUnhandledException(ex)
APIs to prevent unnecessarily repeated debugger stops for the same user-unhandled exceptions in Blazor apps with Just My Code enabled.
Description
This is a follow up to #57148 which I've made after testing Visual Studio's treatment of NavigationExceptions
when debugging with just my code
enabled. The intent was to ensure that NavigationExceptions
that get handled properly by Blazor do not get treated as user-unhandled exceptions. Unfortunately, this didn't fully work because the debugger does not follow exceptions through multiple try-catch blocks. For example:
Sample code demonstrating what the debugger does and does not stop for
// ExternalAssembly.dll public class Class1 { // This attribute works, but only for Funcs that return faulted Tasks. [DebuggerDisableUserUnhandledExceptions] public static async Task RunNestedAsync(Func callback) { try { await Nested(callback); } catch { } }
private static async Task Nested(Func<Task> callback)
{
await callback();
}
// This attribute works for Funcs that return faulted Tasks or throw directly.
[DebuggerDisableUserUnhandledExceptions]
public static async Task RunNestedPassThroughAsync(Func<Task> callback)
{
try
{
await NestedPassThrough(callback);
}
catch
{
}
}
private static Task NestedPassThrough(Func<Task> callback) => callback();
// This attribute does nothing due to the try/catch block in NestedTryCatch.
[DebuggerDisableUserUnhandledExceptions]
public static async Task RunNestedTryCatchAsync(Func<Task> callback)
{
try
{
await NestedTryCatch(callback);
}
catch
{
}
}
private static async Task NestedTryCatch(Func<Task> callback)
{
try
{
await callback();
}
catch
{
throw;
}
}
}
// Program.cs in another assembly app.MapGet("/nested", async () => { await Class1.RunNestedAsync(async () => { // This is successfully ignored as a user-unhandled exception despite async nested method. throw new Exception(); }); });
app.MapGet("/nested-non-async", async () => { await Class1.RunNestedAsync(() => { // However, throwing from a non-"async" callback makes the debugger treat it as user-unhandled again. throw new Exception(); }); });
app.MapGet("/nested-pass-through-non-async", async () => { await Class1.RunNestedPassThroughAsync(() => { // This is successfully ignored as a user-unhandled exception again despite the non-async callback // now that the nested method is non-async. throw new Exception(); }); });
app.MapGet("/nested-try-catch", async () => { await Class1.RunNestedTryCatchAsync(async () => { // A nested method with a try/catch block makes the debugger treat all thrown exceptions // as user-unhandled again regardless of the asyncness of the callback. throw new Exception(); }); });
This caused the debugger to treat most NavigationException
's as user-unhandled exceptions, because the method with [DebuggerDisableUserUnhandledExceptions]
was too far down the stack with multiple try/catch blocks in between. And in some cases, the debugger would stop for exceptions at a try/catch boundary and then extraneously stop again for the same exception because of calls Debugger.BreakForUserUnhandledException(ex)
.
I used the Redirect*.razor
pages in https://github.com/halter73/DebuggerDisableUserUnhandledExceptions/tree/nav/BlazorApp1/Components/Pages and https://github.com/halter73/DebuggerDisableUserUnhandledExceptions/tree/throw/BlazorApp1/Components/Pages to test these changes.
Customer Impact
Without this change, Blazor developers with Just My Code enabled will sometimes see the debugger stop repeatedly for the same user-unhandled exception. They might also see the debugger stop for some NavigationExceptions
which are a normal part of Blazor's operation when the NavigationManager
is used during prerendering.
Regression?
- Yes
- No
Risk
- High
- Medium
- Low
Previously, the debugger would not stop at all for these user-unhandled exceptions. Breaking for Async User-Unhandled exceptions in the Visual Studio Debugger in async code is new in .NET 9.
If exceptions happen to be common in a given developer's workflow, they can easily disable breaking for user-unhandled exceptions either in general or on a per exception type basis to get the old behavior of never stopping for user-unhandled exceptions.
Verification
- Manual (required)
- Automated
Packaging changes reviewed?
- Yes
- No
- N/A