3

I'm having a bit of trouble understanding why the Visual Studio debugger is capable of breaking on unhandled exceptions for events in WPF applications, but not for controller actions in web applications. If I have a handler like so:

private async void SomeButton_Click(object sender, RoutedEventArgs e)
{
    throw new NotImplementedException();
}

The debugger will gladly break on the unhandled exception. However, if I have a similar action on a controller:

public async Task<SomeReturnValue> SomeAction()
{
    throw new NotImplementedException();
}

the debugger passes over the exception and ultimately the MVC Framework handles it and transforms it into a 500 Server Error to return to the client.

It's worth noting that console apps seem to have similar behavior to web apps when it comes to exceptions in async methods. I know that for ASP.NET I can disable Just My Code and break on all thrown exceptions instead of just unhandled exceptions, but I don't have a firm grasp on the architectural differences here that allow us to break on unhandled exceptions for WPF events and not ASP.NET requests. It seems that the exceptions illustrated above are observed exceptions for WPF but unobserved for ASP.NET. Can someone elaborate on why?

Community
  • 1
  • 1
joelmdev
  • 11,083
  • 10
  • 65
  • 89
  • Most of what you're going to get here is going to be guessing and conjecture. Your best bet would be to raise an issue with the appropriate department at Microsoft. – Chris Pratt Oct 28 '15 at 18:08
  • @ChrisPratt there are some pretty knowledgeable individuals on this subject here, at least one of which I've pinged for an answer. – joelmdev Oct 28 '15 at 18:13
  • Well, it was a bit subtle, but the implication of my comment is that this isn't really an appropriate place for this question. It's pretty easily falls under one or both of the "Opinionated" or "Broad" close categories, unless an official representative of Microsoft posts an answer, it's still just a guess. – Chris Pratt Oct 28 '15 at 18:15
  • 1
    @ChrisPratt There's no solicitation for opinion in this question, and the subject is pretty focused, it's just a hard question that not many understand well enough to answer. I don't think a question should be avoided just because most users won't know the answer- quite to the contrary, in fact. If users don't know the answer, they shouldn't answer. – joelmdev Oct 28 '15 at 18:29

1 Answers1

5

.NET exceptions are built on a Win32 concept called structured exception handling (SEH).

SEH has a "two-step" system for managing thrown exceptions. The first step is walking up the stack searching for a handler. This first step is weird because it will execute exception filters before unwinding the stack. (On a side note, exception filters are a new addition to the C# language in C# 6 / VS2015). So the first step walks up the stack executing filters until it finds a catch block with a matching filter. Only then does the second step take place: unwinding the stack and starting execution of the catch block. (On a side note, SEH exception filters can also return a value meaning "resume execution at the point where it was thrown", but this is advanced and esoteric behavior, not available in C#).

The final piece of the puzzle is conjecture on my part, but it seems reasonable: the debugger hooks (or watches / intercepts / whatever) all thread entry points, and if an exception escapes to that point it will kick off its "unhandled exception" workflow. If I had to guess, the debugger implements its "hook" as an SEH filter (at least, it appears to behave like one).

That's sufficient to describe the observed behavior.


For example, consider what happens when a catch block contains a throw; statement:

static void Main()
{
    TestA();
}

static void TestA()
{
    try
    {
        TestB();
    }
    catch
    {
        throw; // Debugger breaks here
    }
}

static void TestB()
{
    throw new InvalidCastException();
}

When the exception is thrown, the first step of SEH searches up the call stack and finds the catch block. Then the second step of SEH unwinds the stack up to that catch block and executes it.

When the throw; executes, it actually kicks off a different SEH exception. The first step of SEH searches up the call stack and triggers the debugger hook. The debugger then stops everything and steps in before the stack is unwound. But note that the debugger breaks at the throw; statement, because that's where the stack actually is.

Now, we did not stomp the stack in the Exception object (in other words, we were sure to use throw; and not throw exception;), and if you examine the exception details you will indeed see the entire stack. This stack was captured and placed on the Exception object at the point it was originally thrown. But the debugger does not examine the Exception object instance; it's only looking at the actual thread stack, which ends at throw;.

Note the difference in behavior with the new exception filters:

static void Main(string[] args)
{
    TestA();
}

static void TestA()
{
    try
    {
        TestB();
    }
    catch when (false)
    {
        throw;
    }
}

static void TestB()
{
    throw new InvalidCastException(); // Debugger breaks here
}

Now, when the exception is thrown, SEH performs its first step and searches up the call stack. It finds the catch but the filter returns false, so it just continues searching and runs into the debugger thread hook. At that point, the debugger stops everything and breaks at the current stack, which is where the exception was thrown. The stack was never unwound to the catch so it doesn't end up breaking there.


So, to apply this to your specific question...

The WPF event handler has nothing between it and the UI thread proc to catch the exception, so any exceptions will take their first step all the way to the debugger hook:

private async void SomeButton_Click(object sender, RoutedEventArgs e)
{
    throw new NotImplementedException();
}

On the controller action, things are quite different:

public async Task<SomeReturnValue> SomeAction()
{
    throw new NotImplementedException();
}

First, the method is returning a Task<T>. When you have an async method that returns a task, the compiler rewriting of the async method will catch any exceptions and just place them on the task. Internally, ASP.NET then observes those exceptions and sends a 500. Even if this was a synchronous method, ASP.NET would still catch your exceptions automatically and translate them into a 500 response.

Those exceptions are never raised all the way to the thread proc. Which is a good thing, because that would cause your entire web app to crash. :)

Unfortunately, this means that the debugger "unhandled exception" workflow is never kicked off, either.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810