Home » C# » Develop a Global Exception handler in a .Net Core API

Develop a Global Exception handler in a .Net Core API

By Emily

I’ve been writing recently about developing an API using .Net Core and C#, and one of the more recent challenges I’ve faced has been implementing better error handling in the code. There are many types of Exception but it’s much easier for anyone consuming your API in a front end application if you return every Exception in a consistent format. This post explains how to customise the different Exception types and return them in a consistent response object. It also describes a few ways of implementing a global exception handler in a .Net Core API.

Different Exception Types

First of all let’s summarise the different Exception types we will need to deal with. We’ll need to be able to handle and format the following:

  1. Unhandled Exceptions
  2. Default Exceptions thrown from ApiController
  3. Exceptions that we deliberately throw (NotFound for example).

I use the APIController default behaviours when I build a .Net Core API as it means you don’t have to worry about handling all sorts of common issues. It does lots of the leg work for you in handling malformed request bodies, missing parameters etc.

We want to make sure that any error is wrapped in a consistent object so that only one main error handling method is required in any system that consumes our API.

Build a single custom Exception wrapper

If we want all the different Exceptions to be consistent, then we need to create a custom object which will be used to wrap each different type of Exception. Create a new model called ApiResponse which has 2 properties – Title, and Status :


using Newtonsoft.Json;

public class ApiResponse
    {
        public int Status { get; }

        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
        public string Title { get; }

        public ApiResponse(int status, string title = null)
        {
            Status = status;
            Title = title ?? GetDefaultMessageForStatusCode(status);
        }

        private static string GetDefaultMessageForStatusCode(int statusCode)
        {
            switch (statusCode)
            {
                case 404:
                    return "Not found";
                case 500:
                    return "An unhandled error occurred";
                default:
                    return null;
            }
        }
    }

This will serve as the single common model and will ensure every Exception that is wrapped by this has a Status and a Title property.

Now we can create a more specific object for each type of status code. Each of these will inherit from the ApiResponse object. That means every Exception will have the two properties inherited from ApiResponse, along with any other properties defined in the child object.

Build a custom BadRequest response object

For a BadRequest we create this object, which includes an additional property called Errors, and a constructor which will build a list of error messages from the ModelStateDictionary passed in :


using Microsoft.AspNetCore.Mvc.ModelBinding;

public class ApiBadRequestResponse : ApiResponse
{
	public List<Error> Errors { get; }

    public ApiBadRequestResponse(ModelStateDictionary modelState)
            : base(400)
    {
		if (modelState.IsValid)
        {
        	throw new ArgumentException("ModelState is invalid", nameof(modelState));
		}

        var result = new List<Error>();
        var badFields = modelState.Where(ms => ms.Value.Errors.Any())
                                            .Select(x => new { x.Key, x.Value.Errors });

        foreach (var badField in badFields)
        {
        	var fieldKey = badField.Key;
            var fieldErrors = badField.Errors
            	.Select(error => new Error(fieldKey, error.ErrorMessage));

                result.AddRange(fieldErrors);
		}

        Errors = result;
	}
}

That means our custom BadRequest 400 Exception will be returned with three properties:

  1. Status – the HTTP status code
  2. Title – a short exception type description
  3. Errors – a list of errors relating to the ModelState

Build custom unhandled exception response object

Now let’s create an object specifically for an unhandled exception.


public class ApiExceptionResponse : ApiResponse
{
        public string Message { get; set; }
        public string Detail { get; set; }

        public ApiExceptionResponse(string message = "", string detail = "")
            : base(500)
        {
            Message = message;
            Detail = detail;
        }
}

That means our custom Unhandled 500 Exception will be returned with four properties:

  1. Status – the HTTP status code
  2. Title – a short exception type description
  3. Message – the short exception description
  4. Detail – the stack trace detail of the Exception

Now that we’ve finished building the wrapper objects we’ll look at how to handle each Exception type, and how to catch and customize it before it’s returned.

Format and customize an unhandled exception

Before we do any customizing, let’s take a look at what a default, unformatted Unhandled Exception looks like when you receive it in Postman. In the example below, the text “Just for debugging” is the Exception message in this case.

 System.Exception: Just for debugging
   at PlantsAPI.Controllers.ServiceController.GetServices() in C:\Users\jbloggs\Repos\PlantsAPI\Controllers\ServiceController.cs:line 73
   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.TaskOfIActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Logged|12_1(ControllerActionInvoker invoker)

....

As you can see this isn’t a very ‘friendly’ way of returning a message and it isn’t easy for a front end application to deal with.

So the first thing we need to do is improve how these Unhandled Exceptions are returned, and to do that we need to catch them before we can format them.

Catch and format an unhandled exception

First we need to set up an Error Controller which is where we will handle the Exception, and then add some code to Program.cs to hook up the controller so we can catch them.

Set up an Error Controller

Set up an ErrorController which contains two endpoints, one which will handle the Exceptions in the development environment (where you want all the details to be returned). The other will catch the Exceptions in all the other environments, where for security reasons you might not want to make the same level of detail available in the response.


[Route("/error-development")]
public IActionResult HandleErrorDevelopment([FromServices] IHostEnvironment hostEnvironment)
{
  var exceptionHandlerFeature =
    HttpContext.Features.Get<IExceptionHandlerFeature>();

  //use the custom ApiExceptionResponse object we've already created
  return new ObjectResult(new ApiExceptionResponse(
    exceptionHandlerFeature.Error.Message,
    exceptionHandlerFeature.Error.StackTrace));
}

[Route("/error")]
public IActionResult HandleError()
{
  var exceptionHandlerFeature =
    HttpContext.Features.Get<IExceptionHandlerFeature>();

  return new ObjectResult(new ApiExceptionResponse(
    exceptionHandlerFeature.Error.Message,
    exceptionHandlerFeature.Error.StackTrace));
}

Edit program.cs to use ErrorController

Now add this code to Program.cs to use the new ErrorController endpoints:

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
    app.UseDeveloperExceptionPage();

    //catches any unhandled exception
    app.UseExceptionHandler("/error-development");
}
else
{
    //catches any unhandled exception
    app.UseExceptionHandler("/error");
}

Once this is done, all Unhandled Exceptions will be returned like this :

{
    "type": "https://tools.ietf.org/html/rfc7231#section-6.6.1",
    "title": "Just for debugging",
    "status": 500,
    "detail": "   at PlantsHAPI.Controllers.ServiceController.GetServices() in C:\\Users\\jbloggs\\Repos\\PlantsAPI\\Controllers\\ServiceController.cs:line 73\r\n   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.TaskOfIActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)\r\n   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.
....
....",
    "traceId": "00-600bb849292cfa748de7c761e5e097ba-fdb3183ca9f8bbde-00"
}

As you can see the exception response now contains 4 clear properties – type, title, status, detail. This is much easier to use in the front end, the status code can be checked, there’s a simple title describing the error, and the detail is still provided.

Customize and format default ApiController Exceptions

The benefit of labelling controllers with the [APIController] tag is that it does lots of work for you. It will automatically check that the request is formatted correctly, and if it isn’t, it will return a BadRequest – without you having to write a single line of C# code.

Before any customisation at all, the default BadRequest response from ApiController looks like this:

A default BadRequest 400 from ApiController in .Net core

It contains lots of useful information but it doesn’t match the format of the other Exceptions, and we could make it easier for the front end to handle.

Use an HttpResponseExceptionFilter to customise the ApiController Exception

We need to build an HttpResponseExceptionFilter. This can be used to intercept a response before it executes, and after it executes. For this use case we need to use the OnActionExecuting method to deal with the response before it executes. This is where an ApiController BadRequest exception will be caught.

   
public class HttpResponseExceptionFilter : IActionFilter, IOrderedFilter
    {
        public int Order => int.MaxValue - 10;

        public void OnActionExecuting(ActionExecutingContext context)
        {
            if (!context.ModelState.IsValid)
            {
               //pass the modelstate in, the object constructor builds 
               //a list of errors from the ModelState
                ApiBadRequestResponse responseObject = new
       				ApiBadRequestResponse(context.ModelState);

                context.Result = new ObjectResult(responseObject)
                {
                    StatusCode = 400,
                };
            }
        }
    }

Now the filter is built we need to implement it in Program.cs by including this code:


//Implement the ExceptionFilter
builder.Services.AddControllers(options =>
{
    options.Filters.Add<HttpResponseExceptionFilter>();
});

Now a BadRequest will look like this:

net core api custom format badrequest exception

Customise Exceptions that we throw

Now for the final – and simplest – use case. How can you customise an Exception thrown within your own code?

Before any formatting a standard Notfound exception looks like this:

Default net core api NotFound 404 Exception

All you have to do is wrap it in the appropriate custom Exception response object.

 if (product == null)
     return NotFound(new ApiResponse(404, $"Id {productId} not found"));

And now it will be retirmed like tis:

Custom format net core api NotFound 404 Exception