Demystifying ASP.NET MVC 5 Error Pages and Error Logging
Published
elmah.io loves this post and since we already use it as part of our official documentation for implementing custom error pages, we've decided to sponsor it. Visit elmah.io - Error Management for .NET web applications using ELMAH, powerful search, integrations with Slack and HipChat, Visual Studio integration, API and much more.
Custom error pages and global error logging are two elementary and yet very confusing topics in ASP.NET MVC 5.
There are numerous ways of implementing error pages in ASP.NET MVC 5 and when you search for advice you will find a dozen different StackOverflow threads, each suggesting a different implementation.
Overview
What is the goal?
Typically good error handling consists of:
-
Human friendly error pages
- Custom error page per error code (e.g.: 404, 403, 500, etc.)
- Preserving the HTTP error code in the response to avoid search engine indexing
- Global error logging for unhandled exceptions
Error pages and logging in ASP.NET MVC 5
There are many ways of implementing error handling in ASP.NET MVC 5. Usually you will find solutions which involve at least one or a combination of these methods:
- HandleErrorAttribute
- Controller.OnException Method
- Application_Error event
- customErrors element in web.config
- httpErrors element in web.config
- Custom HttpModule
All these methods have a historical reason and a justifyable use case. There is no golden solution which works for every application. It is good to know the differences in order to better understand which one is applied best.
Before going through each method in more detail I would like to explain some basic fundamentals which will hopefully help in understanding the topic a lot easier.
ASP.NET MVC Fundamentals
The MVC framework is only a HttpHandler plugged into the ASP.NET pipeline. The easiest way to illustrate this is by opening the Global.asax.cs:
public class MvcApplication : System.Web.HttpApplication
Navigating to the implementation of HttpApplication
will reveal the underlying IHttpHandler
and IHttpAsyncHandler
interfaces:
public class HttpApplication : IComponent, IDisposable, IHttpAsyncHandler, IHttpHandler
ASP.NET itself is a larger framework to process incoming requests. Even though it could handle incoming requests from different sources, it is almost exclusively used with IIS. It can be extended with HttpModules and HttpHandlers.
HttpModules are plugged into the pipeline to process a request at any point of the ASP.NET life cycle. A HttpHandler is responsible for producing a response/output for a request.
IIS (Microsoft's web server technology) will create an incoming request for ASP.NET, which subsequently will start processing the request and eventually initialize the HttpApplication (which is the default handler) and create a response:
The key thing to know is that ASP.NET can only handle requests which IIS forwards to it. This is determined by the registered HttpHandlers (e.g. by default a request to a .htm file is not handled by ASP.NET).
And finally, MVC is only one of potentially many registered handlers in the ASP.NET pipeline.
This is crucial to understand the impact of different error handling methods.
Breaking down the options
HandleErrorAttribute
The HandleErrorAttribute is an MVC FilterAttribute, which can be applied to a class or a method:
namespace System.Web.Mvc
{
[AttributeUsage(
AttributeTargets.Class | AttributeTargets.Method,
Inherited = true,
AllowMultiple = true)]
public class HandleErrorAttribute : FilterAttribute, IExceptionFilter
{
// ...
}
}
It's error handling scope is limited to action methods within the MVC framework. This means it won't be able to catch and process exceptions raised from outside the ASP.NET MVC handler (e.g. exceptions at an earlier stage in the life cycle or errors in other handlers). It will equally not catch an exception if the action method is not part of the call stack (e.g. routing errors).
Additionally the HandleErrorAttribute
only handles 500 internal server errors. For instance this will not be caught by the attribute:
[HandleError]
public ActionResult Index()
{
throw new HttpException(404, "Not found");
}
You can use the attribute to decorate a controller class or a particular action method. It supports custom error pages per exception type out of the box:
[HandleError(ExceptionType = typeof(SqlException), View = "DatabaseError")]]
In order to get the HandleErrorAttribute
working you also need to turn customErrors mode on in your web.config:
<system.web>
<customErrors mode="On" />
</system.web>
Use case
The HandleErrorAttribute
is the most limited in scope. Many application errors will bypass this filter and therefore it is not ideal for global application error handling.
It is a great tool for action specific error handling like additional fault tolerance for a critical action method though.
Controller.OnException Method
The OnException
method gets invoked if an action method from the controller throws an exception. Unlike the HandleErrorAttribute
it will also catch 404 and other HTTP error codes and it doesn't require customErrors to be turned on.
It is implemented by overriding the OnException
method in a controller:
protected override void OnException(ExceptionContext filterContext)
{
filterContext.ExceptionHandled = true;
// Redirect on error:
filterContext.Result = RedirectToAction("Index", "Error");
// OR set the result without redirection:
filterContext.Result = new ViewResult
{
ViewName = "~/Views/Error/Index.cshtml"
};
}
With the filterContext.ExceptionHandled
property you can check if an exception has been handled at an earlier stage (e.g. the HandleErrorAttribute):
if (filterContext.ExceptionHandled)
return;
Many solutions on the internet suggest to create a base controller class and implement the OnException
method in one place to get a global error handler.
However, this is not ideal because the OnException
method is almost as limited as the HandleErrorAttribute
in its scope. You will end up duplicating your work in at least one other place.
Use case
The Controller.OnException
method gives you a little bit more flexibility than the HandleErrorAttribute
, but it is still tied to the MVC framework. It is useful when you need to distinguish your error handling between regular and AJAX requests on a controller level.
Application_Error event
The Application_Error
method is far more generic than the previous two options. It is not limited to the MVC scope any longer and needs to be implemented in the Global.asax.cs file:
protected void Application_Error(Object sender, EventArgs e)
{
var raisedException = Server.GetLastError();
// Process exception
}
If you've noticed it doesn't come from an interface, an abstract class or an overriden method. It is purely convention based, similar like the Page_Load
event in ASP.NET Web Forms applications.
Any unhandeled exception within ASP.NET will bubble up to this event. There is also no concept of routes anymore (because it is outside the MVC scope). If you want to redirect to a specific error page you have to know the exact URL or configure it to co-exist with "customErrors" or "httpErrors" in the web.config.
Use case
In terms of global error logging this is a great place to start with! It will capture all exceptions which haven't been handled at an earlier stage. But be careful, if you have set filterContext.ExceptionHandled = true
in one of the previous methods then the exception will not bubble up to Application_Error
.
However, for custom error pages it is still not perfect. This event will trigger for all ASP.NET errors, but what if someone navigates to a URL which isn't handled by ASP.NET? For example try navigating to http://{your-website}/a/b/c/d/e/f/g. The route is not mapped to ASP.NET and therefore the Application_Error
event will not be raised.
customErrors in web.config
The "customErrors" setting in the web.config allows to define custom error pages, as well as a catch-all error page for specific HTTP error codes:
<system.web>
<customErrors mode="On" defaultRedirect="~/Error/Index">
<error statusCode="404" redirect="~/Error/NotFound"/>
<error statusCode="403" redirect="~/Error/BadRequest"/>
</customErrors>
<system.web/>
By default "customErrors" will redirect a user to the defined error page with a HTTP 302 Redirect response. This is really bad practise because the browser will not receive the appropriate HTTP error code and redirect the user to the error page as if it was a legitimate page. The URL in the browser will change and the 302 HTTP code will be followed by a 200 OK, as if there was no error. This is not only confusing but has also other negative side effects like Google will start indexing those error pages.
You can change this behaviour by setting the redirectMode to "ResponseRewrite":
<customErrors mode="On" redirectMode="ResponseRewrite">
This fixes the initial problem, but will give a runtime error when redirecting to an error page now:
Runtime Error An exception occurred while processing your request. Additionally, another exception occurred while executing the custom error page for the first exception. The request has been terminated.
This happens because "ResponseRewrite" mode uses Server.Transfer under the covers, which looks for a file on the file system. As a result you need to change the redirect path to a static file, for example to an .aspx or .html file:
<customErrors mode="On" redirectMode="ResponseRewrite" defaultRedirect="~/Error.aspx"/>
Now there is only one issue remaining with this configuration. The HTTP response code for the error page is still "200 OK". The only way to fix this is to manually set the correct error code in the .aspx error page:
<% Response.StatusCode = 404; %>
This is already pretty good in terms of custom error pages, but we can do better!
Noticed how the customErrors section goes into the system.web section? This means we are still in the scope of ASP.NET.
Files and routes which are not handled by your ASP.NET application will render a default 404 page from IIS (e.g. try http://{your-website}/not/existing/image.gif).
Another downside of customErrors is that if you use a HttpStatusCodeResult instead of throwing an actual exception then it will bypass the ASP.NET customErrors mode and go straight to IIS again:
public ActionResult Index()
{
return HttpNotFound();
//throw new HttpException(404, "Not found");
}
In this case there is no hack which can be applied to display a friendly error page which comes from customErrors.
Use case
The customErrors setting was for a long time the best solution, but still had its limits. You can think of it as a legacy version of httpErrors, which has been only introduced with IIS 7.0.
The only time when customErrors still makes sense is if you can't use httpErrors, because you are running on IIS 6.0 or lower.
httpErrors in web.config
The httpErrors section is similar to customErrors, but with the main difference that it is an IIS level setting rather than an ASP.NET setting and therefore needs to go into the system.webserver section in the web.config:
<system.webServer>
<httpErrors errorMode="Custom" existingResponse="Replace">
<clear/>
<error
statusCode="404"
path="/WebForms/Index.aspx"
responseMode="ExecuteURL"/>
</httpErrors>
<system.webServer/>
It allows more configuration than customErrors but has its own little caveats. I'll try to explain the most important settings in a nutshell:
- httpErrors can be inherited from a higher level (e.g. set in the machine.config)
- Use the
<remove/>
tag to remove an inherited setting for a specific error code. - Use the
<clear/>
tag to remove all inherited settings. - Use the
<error/>
tag to configure the behaviour for one error code. -
responseMode "ExecuteURL" will render a dynamic page with status code 200.
- The workaround to set the correct error code in the .aspx page works here as well.
- responseMode "Redirect" will redirect with HTTP 302 to a URL.
-
responseMode "File" will preserve the original error code and output a static file.
- .aspx files will get output in plain text.
- .html files will render as expected.
The main advantage of httpErrors is that it is handled on an IIS level. It will literally pick up all error codes and redirect to a friendly error page. If you want to benefit from master pages I would recommend to go with the ExecuteURL approach and status code fix. If you want to have rock solid error pages which IIS can serve even when everything else burns, then I'd recommend to go with the static file approach (preferably .html files).
Use case
This is currently the best place to configure friendly error pages in one location and to catch them all. The only reason not to use httpErrors is if you are still running on an older version of IIS (< 7.0).
Custom HttpModule
Last but not least I would like to quickly touch on custom HttpModules in ASP.NET. A custom HttpModule is not very useful for friendly error pages, but it is a great location to put global error logging in one place.
With a HttpModule you can subscribe to the OnError
event of the HttpApplication
object and this event behaves same way as the Application_Error
event from the Global.asax.cs file. However, if you have both implemented then the one from the HttpModule gets called first.
The benefit of the HttpModule is that it is reusable in other ASP.NET applications. Adding/Removing a HttpModule is as simple as adding or removing one line in your web.config:
<system.webServer>
<modules>
<add name="CustomModule" type="SampleApp.CustomModule, SampleApp"/>
</modules>
</system.webServer>
In fact someone has already created a powerful and reusable error logging module and it is open source and called ELMAH. Be sure to check out elmah.io as well.
If you need to create application wide error logging, I highly recommend to look at this project!
Final words
I hope this overview was helpful in explaining the different error handling approaches and how they are linked together.
Each of the techniques has a certain use case and it really depends on what requirements you have. If you have any further questions feel free to ask me here or via any of the social media channels referenced on my about page.
EDIT: There is a new blog post on error handling in ASP.NET Core.