Last update:
Error handling in ASP.NET Core applications
ASP.NET Core has several built-in features from the error handling department. We have access to few different middlewares and MVC filters. Here’s how they work and when to use which.
First of all, it’s important to distinguish between utilities that detect an erroneous status code and modify the response from those that actually handle exceptions. The former is represented by the StatusCodePagesMiddleware
. The rest from the list below falls into the second category.
- StatusCodePagesMiddleware (UseStatusCodePages, UseStatusCodePagesWithReExecute, UseStatusCodePagesWithRedirects)
- DeveloperExceptionPageMiddleware (UseDeveloperExceptionPage)
- ExceptionHandlerMiddleware (UseExceptionHandler)
- MVC Exception Filters (IExceptionFilter, IAsyncExceptionFilter)
StatusCodePagesMiddleware
Comes in three variants and most commonly used via an extension method on IApplicationBuilder
:
UseStatusCodePages()
by default just returns a simple text message describing the HTTP status. It has overloads that allow customization of this message or even execution of entire new middleware chain (we have access to an IApplicationBuilder instance there)UseStatusCodePagesWithReExecute()
, as the name suggests, will re-execute the request as it had been received with another path. It assumes that there is a middleware or MVC action later in the pipeline that will handle such a request. Note that this re-execution happens only on the server. The client does not have any clue about this (i.e. nothing is returned to the client before the request is re-executed). If you need to know what was the original request path, Andrew Lock can help you.UseStatusCodePagesWithRedirects()
re-executes the request pipeline as well, but contrary toUseStatusCodePagesWithReExecute()
, the client initiates this second execution. First, a redirect status code is sent with the response, then the client requests another page. This approach is less recommended than the previous one as the HTTP status codes won’t indicate any errors (so, you may end up with Google indexing your error pages, for example).
Note that this middleware will not catch any exceptions in your pipeline. So the code below won’t work as desired:
1 2 3 4 5 |
public void Configure(IApplicationBuilder app) { app.UseStatusCodePages("text/plain", "The server returned HTTP {0} status code."); app.Run(ctx => throw new Exception("crash!")); } |
In the example above the exception will not be handled by the application at all, so user will not see the message we prepared.
The following will work just fine:
1 2 3 4 5 6 7 8 9 10 |
public void Configure(IApplicationBuilder app) { app.UseStatusCodePages("text/plain", "The server returned HTTP {0} status code."); app.Run(ctx => { ctx.Response.StatusCode = StatusCodes.Status500InternalServerError; return Task.CompletedTask; }); } |
But when an exception happens we need a way to generate an error code based on it. For this, we can use ExceptionHandlerMiddleware described below.
DeveloperExceptionPageMiddleware
This one is for development only (unless you intend to expose your source code and exception details to the world). It catches any unhandled exception and renders a pretty error page with the line of code that threw, stack trace and few more details about the request.
1 2 3 4 5 6 7 8 9 |
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.Run(ctx => throw new Exception("crashed!")); } |
ExceptionHandlerMiddleware
This can be thought of as a global try-catch block. It can catch exceptions and either re-execute the request using another path or run a custom pipeline.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public void Configure(IApplicationBuilder app) { app.UseExceptionHandler("/error"); app.Use(async (ctx, next) => { if (ctx.Request.Path.StartsWithSegments("/error")) { await ctx.Response.WriteAsync("That's an error."); } else { await next(); } }); app.Run(ctx => throw new Exception("crashed!")); } |
1 2 3 4 5 6 7 8 9 |
public void Configure(IApplicationBuilder app) { app.UseExceptionHandler(exApp => { exApp.Run(async ctx => await ctx.Response.WriteAsync("That's an error.")); }); app.Run(ctx => throw new Exception("crashed!")); } |
The ExceptionHandlerMiddleware
plays nicely with the StatusCodePagesMiddleware
:
1 2 3 4 5 6 7 8 9 10 11 |
public void Configure(IApplicationBuilder app) { app.UseStatusCodePages("text/plain", "Got {0} status code."); app.UseExceptionHandler(exApp => { exApp.Run(async ctx => ctx.Response.StatusCode = StatusCodes.Status500InternalServerError); }); app.Run(ctx => throw new Exception("crashed!")); } |
Just remember to put UseStatusCodePages
and UseExceptionHandler
as close to the top of the Configure
method as possible. Any code that’s above won’t be protected by this middleware.
MVC Exception Filters
Having all the above error handling middleware, there should be no need for something else. Yet MVC has its own means of handling errors in action methods – exception filters. It’s still perfectly valid (and even recommended) to use error handling middleware with MVC but when you need to have access to MVC context, exception filters would be the way to go.
There are two interfaces we can implement to create our own filter: IExceptionFilter and IAsyncExceptionFilter. But in reality, instead of writing these classes from scratch, we can just subclass ExceptionFilterAttribute and override its OnException or OnExceptionAsync methods.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class MyErrorHandler : ExceptionFilterAttribute { public override void OnException(ExceptionContext context) { var actionName = context.ActionDescriptor.DisplayName; context.Result = new ContentResult { Content = $"An error occurred in the {actionName} action", ContentType = "text/plain", StatusCode = StatusCodes.Status500InternalServerError }; } } |
Wrapping up
In real life projects I often use a combination of all the middlewares I described. Very seldom I need to write my own MVC exception filter. Here’s my usual setup:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.UseStatusCodePagesWithReExecute("/errors/{0}"); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler(a => { a.Run(ctx => { ctx.Response.StatusCode = StatusCodes.Status500InternalServerError; return Task.CompletedTask; }); }); } // application middlewares (static files, MVC, etc.) go here app.Run(context => { // this prevents IIS to handle 404 error when // no middleware can handle the request context.Response.StatusCode = StatusCodes.Status404NotFound; return Task.CompletedTask; }); } |
Should UseStatusCodePages middleware always be called before the UseExceptionHandler?
I wouldn’t say always, as I can’t imagine all the possible use cases, but it makes sense to do so. UseExceptionHandler catches any unhandled exceptions and sets a status code, then UseStatusCodePages can prepare a response based on the status code.