Adding Global Error Handling and Logging in ASP.NET Core with IExceptionHandlerPathFeature

tldr;

In ASP.NET Core you can hook up a route to be your global exception handler.  Simply add the ExceptionHandler middleware to your Startup.cs Configure method:


// when an exception occurs, route to /Home/Error
app.UseExceptionHandler("/Home/Error");

view raw

Startup.cs

hosted with ❤ by GitHub

In your HomeController’s Error action, add this code to get the exception that was raised, as well as the route that caused the exception, and then you can do something with it (log to a database, send an email/text/fax/carrier pidgeon).


using System;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;
namespace GlobalExceptionHandling.Controllers
{
public class HomeController : Controller
{
public IActionResult Error()
{
// Get the details of the exception that occurred
var exceptionFeature = HttpContext.Features.Get<IExceptionHandlerPathFeature>();
if (exceptionFeature != null)
{
// Get which route the exception occurred at
string routeWhereExceptionOccurred = exceptionFeature.Path;
// Get the exception that occurred
Exception exceptionThatOccurred = exceptionFeature.Error;
// TODO: Do something with the exception
// Log it with Serilog?
// Send an e-mail, text, fax, or carrier pidgeon? Maybe all of the above?
// Whatever you do, be careful to catch any exceptions, otherwise you'll end up with a blank page and throwing a 500
}
return View();
}
}
}

You can checkout this GitHub commit if you’d prefer to see it that way.

 

The start of the non-tldr;

No ELMAH in ASP.NET Core… yet

In pre-Core versions of ASP.NET, ELMAH was a popular library for doing Global Exception Handling.  When unhandled exceptions occurred, it let you do things like log them to a database, send an e-mail, etc. automagically.  Unfortunately, ELMAH is not ported to ASP.NET Core yet, so this led me to look at what other options were out there.

You could use one of the many of the Cloud providers for this like Application Insightselmah.io (unaffiliated with the ELMAH project) or Exceptionless, but what if your company requires you to stay on-prem?  You can use Exception Filters, but those have some limitations, and I found a simpler way (arguably) that I didn’t find a blog post on, so I thought I’d write one up.

 

Step 1 – Hook up the ExceptionHandler middleware

If you’ve done much with ASP.NET Core, this will look very familiar to you.  First you need to hook up the ExceptionHandler middleware by adding this line to the Configure method of your Startup.cs:


// when an exception occurs, route to /Home/Error
app.UseExceptionHandler("/Home/Error");

view raw

Startup.cs

hosted with ❤ by GitHub

What that does is anytime an uncaught exception happens, it will send the request to the route listening on the path you  specified.  In the example above (and the default in the templates), that’s the Error Action on the Home Controller, obviously you can just change that to whatever you want.

Often, you’ll only want to use the Exception Handler in non-development environments, and instead use the DeveloperExceptionPage in Development, which is the new YSOD that gives you a stack trace and other good information.

A sample Configure method showing this is below:


public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
// other code removed for brevity
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// when an exception occurs, route to /Home/Error
app.UseExceptionHandler("/Home/Error");
}
// other code removed for brevity
}

view raw

Startup.cs

hosted with ❤ by GitHub

 

Step 2 – Use IExceptionHandlerPathFeature to get the exception and the Path

Make sure you have the Microsoft.AspNetCore.Diagnostics NuGet package in your .csproj, or if you have the Microsoft.AspNetCore meta-package, then that contains the Microsoft.AspNetCore.Diagnostics package, so you’re all set.

In the action of the controller you’re listening on (in the above case, the Error action on the Home Controller).  Add the following code to your Error action.


using System;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;
namespace GlobalExceptionHandling.Controllers
{
public class HomeController : Controller
{
public IActionResult Error()
{
// Get the details of the exception that occurred
var exceptionFeature = HttpContext.Features.Get<IExceptionHandlerPathFeature>();
if (exceptionFeature != null)
{
// Get which route the exception occurred at
string routeWhereExceptionOccurred = exceptionFeature.Path;
// Get the exception that occurred
Exception exceptionThatOccurred = exceptionFeature.Error;
// TODO: Do something with the exception
// Log it with Serilog?
// Send an e-mail, text, fax, or carrier pidgeon? Maybe all of the above?
// Whatever you do, be careful to catch any exceptions, otherwise you'll end up with a blank page and throwing a 500
}
return View();
}
}
}

Breaking the code down

The code is pretty straight forward, but let’s break it down.  When an exception occurs, an IExceptionHandlerPathFeature is set.  This contains two properties: 1) the Path of the route at which the exception occurred (such as /Home/Index), and 2) the Exception that occurred under the Error property.  After that, you can do whatever you want with this information such as log it to a database, send an e-mail or trigger a carrier pidgeon (I believe the CarrierPidgeon API is coming back in .NET Standard 2.0).

If you want to see the code that makes IExceptionHandlerPathFeature work, you can view go to GitHub here and see the code where they set the Path in the ExceptionHandler middleware that we rigged up in our Startup class.

 

Make sure you get the right Feature

Be careful to make sure you get an IExceptionHandlerPathFeature with the Path in the name.  There is also a IExceptionHandlerFeature without Path in the name that does not have the Path of where the exception occurred.  Obviously, it’s pretty useful to know the source route of where the exception happened.

 

Make sure you catch any Exceptions in your Exception Handler Action

Also be careful and make sure to catch any exceptions in your Exception Handler Action.  Causing a second exception in your exception handler action will cause the entire middleware to stop and you’ll end up with a blank page and a 500 status code.  A scenario where this could happen is if you’re logging the exception to a database, but your database is down causing an exception.

 

Microsoft.AspNetCore.Diagnostics.Elm

Microsoft provides a package called ELM (Error Logging Middleware) that has some similar features to what ELMAH did.  One of the main features ELM has is it allows you to see events on a web page that aggregates those events.  You can read more about that here.  However, there doesn’t seem to be any intent to extend ELM to do things like report to a database or send e-mails, based on this response from Eilon of the ASP.NET team here, and it is non-persistent through app restarts.  In my scenarios, we don’t have a ton of use for this, as we prefer to be able to look and query for logs in a database, but I thought I’d mention it in case it is useful for your scenarios.

 

Additionally – AppDomains Coming Back in .NET Standard 2.0

If you’re using .NET Core (and not full framework), AppDomains are coming back in .NET Standard 2.0 to help with global exception handling, as described by Damian Edwards in the ASP.NET Community Standup here: https://youtu.be/_3y0QsXj2e4?t=31m17s 

 

Don’t like using IExceptionHandlerPathFeature in your Controller action?

That’s ok.  There’s another way to achieve this same thing using raw middleware, even bypassing IExceptionHandlerPathFeature completely, but I’ll save that for a followup blog post, as this is approaching 1000 words.

Thanks for reading!

11 thoughts on “Adding Global Error Handling and Logging in ASP.NET Core with IExceptionHandlerPathFeature

  1. Thanks for the blog post, it was a good read! I work on the Exceptionless project, which is 100% open source. It’s worth pointing out that you can self host on prem with 0 restrictions :).

  2. Note that if you are using any logging middleware (either the built-in debug or console) or one of the third party ones then exceptions will automatically get logged and there is no need for any explicit logging code in your error action.

  3. Is it possible to not return a status code 500 when returning from a custom error page? For example app.UseExceptionHandler(“/Home/Error”) runs this code Json(new{propA = 1, propB = “msg”});

    The Json is actually returned but with status 500, I was trying to get it as 200 instead.

      • As far I tested, there is no way to change the status of the server after it throw a unhandled exception.
        You will get your JSon message, but the status will be 500. And you can’t change it in the middle ware.

        Like a normal .NET web application, when you have an event handler to the OnError, you need to clear the error than redirect to the page.

  4. thanks for the post, one question – In WebApi 2 with IExceptionLogger implementation , it checks if exception can be handled or not if not then just logs it . how this can be implemented here ? since you mentioned to get it from exception and log it. If the exception handler itself didn’t get called due to break in traffic then handler won’t get called.. so how to log exception in that case…

    please clarify , thank you !!

Leave a Reply