Life of Exception in ASP.NET


In this post I’m going to show you the way ASP.NET (MVC) handles exceptions that occur in web applications. We will also examine different places where we can hook our own loggers. Our application will be a very basic ASP.NET MVC project with one controller and one view:

using System;
using System.Web;
using System.Web.Mvc;

[HandleError]
public class HomeController : Controller
{
    //
    // GET: /Home/
    public ActionResult Index()
    {
        return View();
    }
    
    public ActionResult Exception() {
        throw new Exception("test exception");
    }
}
@{
    ViewBag.Title = "test title";
}

<h2>Index page</h2>

<a href=&quot;@Url.Action(&quot;Exception&quot;)&quot;>throw exception</a>

Let’s attach windbg to the IIS Express server hosting our sample application, configure it to stop on all first chance CLR exceptions and break when HandleError filter is called:

0:030> sxe clr
0:027> !Name2EE System.Web.Mvc.dll System.Web.Mvc.HandleErrorAttribute.OnException
Module:      645c1000
Assembly:    System.Web.Mvc.dll
Token:       060009a3
MethodDesc:  645f13d4
Name:        System.Web.Mvc.HandleErrorAttribute.OnException(System.Web.Mvc.ExceptionContext)
JITTED Code Address: 646c94e4
0:027> bp 646c94e4
*** WARNING: Unable to verify checksum for C:\Windows\assembly\NativeImages_v4.0.30319_32\System.Web.Mvc\1a2d7693f4ae1edffeb277679caefefe\System.Web.Mvc.ni.dll

ASP.NET MVC (HandleError)

After clicking the throw exception link windbg should notify us of the exception that was thrown. We may verify that it’s our exception and press go. We should now stop at our breakpoint. A quick look at the stack and we can find that the HandleError filter is called by ControllerActionInvoker:

0:027> !pe
Exception object: 03e03a68
Exception type:   System.Exception
Message:          test exception
InnerException:   <none>
StackTrace (generated):
<none>
StackTraceString: <none>
HResult: 80131500
0:027> g
Breakpoint 0 hit
0:027> !DumpStack
OS Thread Id: 0x16b0 (27)
Current frame: (MethodDesc 645f13d4 +0 System.Web.Mvc.HandleErrorAttribute.OnException(System.Web.Mvc.ExceptionContext))
ChildEBP RetAddr  Caller, Callee
1058ec68 646ab72e (MethodDesc 645eb840 +0x6e System.Web.Mvc.ControllerActionInvoker.InvokeExceptionFilters(System.Web.Mvc.ControllerContext, System.Collections.Generic.IList`1<System.Web.Mvc.IExceptionFilter>, System.Exception)), calling clr!VSD_FixupAsmStub
1058ec94 646aae06 (MethodDesc 645eb7e8 +0x13a System.Web.Mvc.ControllerActionInvoker.InvokeAction(System.Web.Mvc.ControllerContext, System.String))
1058ecc0 646aac1c (MethodDesc 645eb7f8 +0xb4 System.Web.Mvc.ControllerActionInvoker.InvokeActionMethodFilter(System.Web.Mvc.IActionFilter, System.Web.Mvc.ActionExecutingContext, System.Func`1<System.Web.Mvc.ActionExecutedContext>)), calling clr!IL_Rethrow
1058ed84 646ab3b5 (MethodDesc 645eb744 +0x61 System.Web.Mvc.ControllerActionInvoker..ctor()), calling clr!JIT_WriteBarrierEAX
1058ed98 646b10ab (MethodDesc 645ecb44 +0x6b System.Web.Mvc.Controller.ExecuteCore()), calling 03859182
1058edc0 646ab9fc (MethodDesc 645eb97c +0x5c System.Web.Mvc.ControllerBase.Execute(System.Web.Routing.RequestContext))
1058ede8 646abc6b (MethodDesc 645eb9b0 +0xb System.Web.Mvc.ControllerBase.System.Web.Mvc.IController.Execute(System.Web.Routing.RequestContext))
1058edf0 646b735b (MethodDesc 645edca0 +0x23 System.Web.Mvc.MvcHandler+<>c__DisplayClass6+<>c__DisplayClassb.<BeginProcessRequest>b__5()), calling 0385f75a
1058ee10 646afb44 (MethodDesc 645ec504 +0x14 System.Web.Mvc.Async.AsyncResultWrapper+<>c__DisplayClass1.<MakeVoidDelegate>b__0())
1058ee1c 646cf5cb (MethodDesc 645f262c +0xb System.Web.Mvc.Async.AsyncResultWrapper+<>c__DisplayClass8`1[[System.Web.Mvc.Async.AsyncVoid, System.Web.Mvc]].<BeginSynchronous>b__7(System.IAsyncResult))
1058ee20 646afdf7 (MethodDesc 645ec57c +0x3f System.Web.Mvc.Async.AsyncResultWrapper+WrappedAsyncResult`1[[System.Web.Mvc.Async.AsyncVoid, System.Web.Mvc]].End())
1058ee34 646b7231 (MethodDesc 645edc40 +0x31 System.Web.Mvc.MvcHandler+<>c__DisplayClasse.<EndProcessRequest>b__d()), calling (MethodDesc 645ec57c +0 System.Web.Mvc.Async.AsyncResultWrapper+WrappedAsyncResult`1[[System.Web.Mvc.Async.AsyncVoid, System.Web.Mvc]].End())
1058ee44 6469d398 (MethodDesc 645e9248 +0x8 System.Web.Mvc.SecurityUtil.<GetCallInAppTrustThunk>b__0(System.Action))
1058ee48 6469d317 (MethodDesc 645e923c +0x17 System.Web.Mvc.SecurityUtil.ProcessInApplicationTrust(System.Action))
1058ee54 646b6cd5 (MethodDesc 645edb1c +0x3d System.Web.Mvc.MvcHandler.EndProcessRequest(System.IAsyncResult)), calling (MethodDesc 645e923c +0 System.Web.Mvc.SecurityUtil.ProcessInApplicationTrust(System.Action))
1058ee64 646b698a (MethodDesc 645edba0 +0xa System.Web.Mvc.MvcHandler.System.Web.IHttpAsyncHandler.EndProcessRequest(System.IAsyncResult))
1058ee6c 65bec82d (MethodDesc 6511b544 System.Web.HttpApplication+CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()), calling 0385ae7e
1058eeb0 65298aec (MethodDesc 65113f2c +0x9c System.Web.HttpApplication.ExecuteStep(IExecutionStep, Boolean ByRef)), calling 03858d22
1058eef0 652ac310 (MethodDesc 6511b4f4 +0x474 System.Web.HttpApplication+PipelineStepManager.ResumeSteps(System.Exception)), calling (MethodDesc 65113f2c +0 System.Web.HttpApplication.ExecuteStep(IExecutionStep, Boolean ByRef))
1058ef74 65298d50 (MethodDesc 65113f8c +0x60 System.Web.HttpApplication.BeginProcessRequestNotification(System.Web.HttpContext, System.AsyncCallback))
1058ef88 65294e2b (MethodDesc 6510de78 +0xbb System.Web.HttpRuntime.ProcessRequestNotificationPrivate(System.Web.Hosting.IIS7WorkerRequest, System.Web.HttpContext)), calling (MethodDesc 65113f8c +0 System.Web.HttpApplication.BeginProcessRequestNotification(System.Web.HttpContext, System.AsyncCallback))
1058efd8 6529ad2d (MethodDesc 6510fad4 +0x245 System.Web.Hosting.PipelineRuntime.ProcessRequestNotificationHelper(IntPtr, IntPtr, IntPtr, Int32)), calling (MethodDesc 6510de78 +0 System.Web.HttpRuntime.ProcessRequestNotificationPrivate(System.Web.Hosting.IIS7WorkerRequest, System.Web.HttpContext))
1058f06c 6529aabf (MethodDesc 6510fac8 +0x1f System.Web.Hosting.PipelineRuntime.ProcessRequestNotification(IntPtr, IntPtr, IntPtr, Int32)), calling (MethodDesc 6510fad4 +0 System.Web.Hosting.PipelineRuntime.ProcessRequestNotificationHelper(IntPtr, IntPtr, IntPtr, Int32))
1058f094 0382a98a 0382a98a
...

As we can read from the stack trace, InvokeExceptionFilter is called from the InvokeAction method. Here is the decompiled code to check how the handling is defined:

FilterInfo filters = this.GetFilters(controllerContext, actionDescriptor);
try
{
  AuthorizationContext authorizationContext = this.InvokeAuthorizationFilters(controllerContext, filters.AuthorizationFilters, actionDescriptor);
  if (authorizationContext.Result != null)
  {
    this.InvokeActionResult(controllerContext, authorizationContext.Result);
  }
  else
  {
    if (controllerContext.Controller.ValidateRequest)
    {
      ControllerActionInvoker.ValidateRequest(controllerContext);
    }
    IDictionary<string, object> parameterValues = this.GetParameterValues(controllerContext, actionDescriptor);
    ActionExecutedContext actionExecutedContext = this.InvokeActionMethodWithFilters(controllerContext, filters.ActionFilters, actionDescriptor, parameterValues);
    this.InvokeActionResultWithFilters(controllerContext, filters.ResultFilters, actionExecutedContext.Result);
  }
}
catch (ThreadAbortException)
{
  throw;
}
catch (Exception exception)
{
  ExceptionContext exceptionContext = this.InvokeExceptionFilters(controllerContext, filters.ExceptionFilters, exception);
  if (!exceptionContext.ExceptionHandled)
  {
    throw;
  }
  this.InvokeActionResult(controllerContext, exceptionContext.Result);
}
return true;

The default HandleErrorAttribute swallows exceptions inherit from the exception type specified in its constructor (by default it’s System.Exception so all exceptions are included). After setting the exceptionContext.ExceptionHandled to true, the ControllerActionInvoker happily continues using ActionResult provided by the HandleErrorAttribute (by default it’s an action of rendering the Error.cshtml view). This also means that no other ASP.NET components are notified about the problem that occured. You may ask what other components am I talking about? We can find out by examining the stack. .NET exception handling is based on SEH (Structure Exception Handling). I won’t go too deep into this matter as it’s quite complicated and differs among architectures (x64, x86), but what’s important for us is that by checking the stack from top to bottom we are able to identify all exception handles (catch blocks) waiting for exceptions that happen “above them”. Imagine that our HandleErrorAttribute hasn’t caught the exception. In that case the framework starts looking for another catch block which will be able to handle the exception. We can emulate this behavior using !EHInfo command from the SOS extension on all method descriptors found in the stack trace presented at the beginning of this post (I “dotted” methods that do not have any catch clauses defined):

0:027> !EHInfo 645eb7e8
MethodDesc:   645eb7e8
Method Name:  System.Web.Mvc.ControllerActionInvoker.InvokeAction(System.Web.Mvc.ControllerContext, System.String)
Class:        645dd248
MethodTable:  646f1b1c
mdToken:      06000269
Module:       645c1000
IsJitted:     yes
CodeAddr:     646aaccc
Transparency: Transparent

EHHandler 0: TYPED 
Clause:  [646aad4b, 646aadeb] [7f, 11f]
Handler: [646aadeb, 646aadf0] [11f, 124]

EHHandler 1: TYPED 
Clause:  [646aad4b, 646aadeb] [7f, 11f]
Handler: [646aadf0, 646aae44] [124, 178]
...
0:027> !EHInfo 6511b544
MethodDesc:   6511b544
Method Name:  System.Web.HttpApplication+CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
Class:        65102264
MethodTable:  6533f124
mdToken:      06002441
Module:       650e1000
IsJitted:     yes
CodeAddr:     652bdc10
Transparency: Safe critical

EHHandler 0: TYPED 
Clause:  [652bde25, 652bde3f] [215, 22f]
Handler: [652bdd07, 652bdd3e] [f7, 12e]

EHHandler 1: FINALLY 
Clause:  [652bde3f, 652bde74] [22f, 264]
Handler: [652bdd3e, 652bdd5c] [12e, 14c]

EHHandler 2: FINALLY 
Clause:  [652bdd5c, 652bdd7b] [14c, 16b]
Handler: [652bdd7b, 652bde14] [16b, 204]
...
0:027> !EHInfo 65113f2c
MethodDesc:   65113f2c
Method Name:  System.Web.HttpApplication.ExecuteStep(IExecutionStep, Boolean ByRef)
Class:        65100b00
MethodTable:  6533721c
mdToken:      060023e0
Module:       650e1000
IsJitted:     yes
CodeAddr:     65298a50
Transparency: Safe critical

EHHandler 0: FINALLY 
Clause:  [65298a8d, 65298aab] [3d, 5b]
Handler: [65298aab, 65298acb] [5b, 7b]

EHHandler 1: TYPED 
Clause:  [65298a73, 65298b08] [23, b8]
Handler: [65298b08, 65298b8e] [b8, 13e]

EHHandler 2: TYPED 
Clause:  [65298a73, 65298b08] [23, b8]
Handler: [65298b8e, 65298b98] [13e, 148]

EHHandler 3: TYPED 
Clause:  [65298a73, 65298b98] [23, 148]
Handler: [65298b98, 65298c39] [148, 1e9]
... 
0:027> !EHInfo 6510de78
MethodDesc:   6510de78
Method Name:  System.Web.HttpRuntime.ProcessRequestNotificationPrivate(System.Web.Hosting.IIS7WorkerRequest, System.Web.HttpContext)
Class:        650ff9d8
MethodTable:  6533220c
mdToken:      0600294b
Module:       650e1000
IsJitted:     yes
CodeAddr:     65294d70
Transparency: Safe critical

EHHandler 0: TYPED 
Clause:  [65294e45, 65294e52] [d5, e2]
Handler: [65294e52, 65294e74] [e2, 104]

EHHandler 1: TYPED 
Clause:  [65294d95, 65294f9d] [25, 22d]
Handler: [65294f9d, 65294ff5] [22d, 285]
...
0:027> !EHInfo 6510fac8
MethodDesc:   6510fac8
Method Name:  System.Web.Hosting.PipelineRuntime.ProcessRequestNotification(IntPtr, IntPtr, IntPtr, Int32)
Class:        650e15d0
MethodTable:  65332ca8
mdToken:      06002169
Module:       650e1000
IsJitted:     yes
CodeAddr:     6529aaa0
Transparency: Safe critical

EHHandler 0: TYPED 
Clause:  [6529aab4, 6529aac1] [14, 21]
Handler: [6529aac1, 6529aad8] [21, 38]

ASP.NET (http modules, customerrors)

We’ve already checked the first method from the above list. Now it’s time for: System.Web.HttpApplication+CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute(). This method posts a request to the handler and rethrows the exception if any occurs. Next on the list is System.Web.HttpApplication.ExecuteStep(IExecutionStep, Boolean ByRef). After decompiling we can see that it handles any exception gracefully, sending it as a result to the caller:

internal Exception ExecuteStep(HttpApplication.IExecutionStep step, ref bool completedSynchronously)
{
	Exception result = null;
  ...
  try
  {
    ...
    step.Execute();
    ...
  }
  catch (Exception ex)
  {
    result = ex;
    ...
  }
  catch
  {
  }
  ....
  return result;
}

Now, the caller which happens to be System.Web.HttpApplication+PipelineStepManager.ResumeSteps(System.Exception)

internal override void ResumeSteps(Exception error)
{
  ...
  while (true)
  {
    if (syncContext.Error != null)
    {
      error = syncContext.Error;
      syncContext.ClearError();
    }
    if (error != null)
    {
      this._application.RecordError(error);
      error = null;
    }
    if (!this._validateInputCalled || !this._validatePathCalled)
    {
      error = this.ValidateHelper(context);
      if (error != null)
      {
        continue;
      }
    }
    if (syncContext.PendingCompletion(this._resumeStepsWaitCallback))
    {
      break;
    }
    ...
    error = this._application.ExecuteStep(nextEvent, ref flag4);
    ...
  }
  ...
}

checks if an error occured while processing the request (any of the executed steps returns something different than null). If so, the PipelineStepManager records it and notifies all error event handlers (this._application.RecordError(error)), including http modules which subscribed to this type of events (httpContext.Error += new EventHandler(httpModule_Error)). If event handlers do not clear the error information in Http context (httpContext.Server.ClearError()), the error will be eventually handled by HttpRuntime (more exactly System.Web.HttpContext.ReportRuntimeErrorIfExists(System.Web.RequestNotificationStatus ByRef)). HttpRuntime creates an ASP.NET Health Monitoring event (by default it logs the exception to the Application event log) and prepares a Yellow Screen Of Death, taking into consideration customErrors settings from the web.config file.

I hope after having read this post you have a better understanding of the exception flow in ASP.NET and, next time if Elmah does not report any exceptions in ASP.NET MVC application you will know where to look for the fault 🙂

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s