This post gives a better understanding of the exception flow in ASP.NET. If, in future, Elmah does not report any exceptions in ASP.NET MVC application, you will know where to look for the fault.
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
{
public ActionResult Index()
{
return View();
}
public ActionResult Exception() {
throw new Exception("test exception");
}
}
@{
ViewBag.Title = "test title";
}
<h2>Index page</h2>
<a href="@Url.Action("Exception")">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 exception 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. 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 inheriting 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 occurred. You may ask what other components I am talking about. We can find it 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) awaiting for exceptions that happen “above them”. Imagine that our HandleErrorAttribute
hasn’t caught the exception. In that case, 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 exception if any occurs. Next on the list is System.Web.HttpApplication.ExecuteStep(IExecutionStep, Boolean ByRef)
. After decompiling, we can see that it gracefully handles any exception, 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 occurred 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 these 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 that 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
.