.NET Global Exception Handling: 3 Techniques Beyond Try/Catch Blocks

 


Exceptions are a big part of any software solution. They happen sometimes because something went wrong like out of memory exceptions and sometimes, they happen because of the domain or the business such as validation exceptions. Either way it is something that must be planned ahead in any system design. And by planning I definitely don't mean sprinkling try/catch blocks all over your code.

In this post I will explain three different techniques that will help you to globally handle exceptions and turn the messy response that exceptions return by default to a much organized and concise response. However, note that this does not mean that you should not stop using try/catch blocks if you need them for specific exception handling for specific cases.

Our main focus is to manage to run some sort of logic that runs each time an exception occurs so that it prevents the default behavior and instead logs the full exception stack trace and only returns concise readable information for the user of the application like this:


Instead of this:


That being said, let's explore these 3 approaches for exception handling.

1. Middleware

I have talked about middlewares before in my previous post Google & Microsoft Token Validation Middleware in ASP.NET Applications. They're a very helpful technique that allows you intercept the flow and pipeline of your application and opens the door for a lot of useful ways to help you manage your application. One of these ways is of course exception handling.

Because middlewares eventually call their next step in the context it is safe to think that the rest of processing can happen in the line where the next step is called so if you wrap this line of code in a try/catch block this would mean that you are handling any exception the happens within the application context (after registering the middleware).

And as agreed our aim is to log our exception because we need all information about this exception to be logged by our logger and beautify the response then return it. So, let's put this on paper.

Let's first create the middleware which will perform all the needed work.

using System.Text.Json;

namespace HARDCODE.API.Middlewares
{
    public class ExceptionHandlingMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly ILogger _logger;

        public ExceptionHandlingMiddleware(RequestDelegate next, ILogger logger)
        {
            _next = next;
            _logger = logger;
        }

        public async Task Invoke(HttpContext context)
        {
            try
            {
                await _next(context);
            }
            catch (Exception ex)
            {
                // Log exception here
                _logger.LogInformation("An exception occurred");
                _logger.LogError(ex, ex.Message);

                // Perform necessary error handling logic here
                await HandleExceptionAsync(context, ex);
            }
        }

        private static Task HandleExceptionAsync(HttpContext context, Exception exception)
        {
            var contextFeature = exception;
            int statusCode;
            object response;
            switch (contextFeature)
            {
                case UnauthorizedAccessException:
                    statusCode = StatusCodes.Status401Unauthorized;
                    response = new
                    {
                        title = "Unauthorized",
                        status = statusCode,
                        message = contextFeature.Message
                    };
                    break;
                default:
                    statusCode = StatusCodes.Status500InternalServerError;
                    response = new
                    {
                        title = "Internal server error",
                        status = statusCode,
                        message = contextFeature.Message,
                    };
                    break;
            }
            context.Response.ContentType = "application/json";
            context.Response.StatusCode = statusCode;
            return context.Response.WriteAsync(JsonSerializer.Serialize(response));
        }
    }

}
So, as you can see firstly, we log the error using the ILogger which is injected then we call the function that handles the exception by beautifying the response and creating a json object that has a status code, title and a message. You can switch between different types of exception, custom or out of the box if you need any special handling for certain types of exceptions.

Now that that's added let's register this middleware in our program.cs.
app.UseMiddleware<ExceptionHandlingMiddleware>();

As you can see it's just a simple line which will add your middleware and injects the needed dependencies and after that you're good to go and now you have a global exception handling service that made your response readable and elegant, and your error is now logged, and you can open your logs and find out the specific details of the exception that occurred.

But you know, using a middleware is a bit generic for exception handling. What about if I needed a more specific approach. One which seems more in the right place like instead of UseMiddleware we can apply our logic in UseExceptionHandler.

2. Extension

Now I know it's tempting to use UseExceptionHandler as it is but understand that to use this -out of the box- middleware we must first extend the IApplicationBuilder in a separate class to encapsulate the logic rather than just having it just lying around in program.cs.

So, let's create a class called ExceptionHandlingExtension which includes a member function ConfigureExceptionHandler that allows us to use the UseExceptionHandler middleware.

using Microsoft.AspNetCore.Diagnostics;

using System.Text.Json;

namespace HARDCODE.API.Extensions

{

    public static class ExceptionHandlingExtension

    {

        public static void ConfigureExceptionHandler(this IApplicationBuilder app, ILogger logger)

        {

            app.UseExceptionHandler(appError =>

            {

                appError.Run(async context =>

                {

                    var contextFeature = context.Features.Get<IExceptionHandlerFeature>();


                    if (contextFeature != null)

                    {

                        // Log exception here

                        logger.LogInformation("An exception occurred");

                        logger.LogError(contextFeature.Error, contextFeature.Error.Message);


                        // Perform necessary error handling logic here

                        int statusCode;

                        object response;

                        switch (contextFeature.Error)

                        {

                            case UnauthorizedAccessException:

                                statusCode = StatusCodes.Status401Unauthorized;

                                response = new

                                {

                                    title = "Unauthorized",

                                    status = statusCode,

                                    message = contextFeature.Error.Message

                                };

                                break;

                            default:

                                statusCode = StatusCodes.Status500InternalServerError;

                                response = new

                                {

                                    title = "Internal server error",

                                    status = statusCode,

                                    message = contextFeature.Error.Message,

                                };

                                break;

                        }

                        context.Response.ContentType = "application/json";

                        context.Response.StatusCode = statusCode;

                        await context.Response.WriteAsync(JsonSerializer.Serialize(response));

                    }

                });

            });

        }

    }

}

As you can see it pretty much does the same thing as the first approach it's just different set-up. The logger should be injected manually as it will not be injected automatically like the middleware in the first approach.

This is how you register this service in program.cs and notice how we have to manually inject the logger. 

app.ConfigureExceptionHandler(app.Logger);
The two approaches do the same thing identically in terms of output and honestly there's no significant difference between them rather than the first approach is a little bit more general and can involve more logic and isn't really obligated to only handle exception. When in the second approach the logic you add in the UseExceptionHandler scope must be related to exception handling.

A third approach is one that is quite different than middlewares but it serves the same function in the case of exception handling.

3. Filters


Filters are closely linked to specific MVC actions. They allow you to add functionality that is specific to an action or controller.
The execution order of filters is after middlware. In summary, middleware is used for processing the entire request pipeline and is configured globally or per-route, while filters are applied to controllers or actions and provide more control over request processing. 

So, let's see how handle exceptions using filters.

using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc;

namespace HARDCODE.API.Filters
{
    public class GlobalExceptionFilter : IExceptionFilter
    {
        private readonly ILogger<GlobalExceptionFilter> _logger;

        public GlobalExceptionFilter(ILogger<GlobalExceptionFilter> logger)
        {
            _logger = logger;
        }

        public void OnException(ExceptionContext context)
        {
            var exception = context.Exception;

            if(exception == null)
            {
                return;
            }   

            // Log exception here
            _logger.LogInformation("An exception occurred");
            _logger.LogError(exception, exception.Message);

            // Perform necessary error handling logic here
            int status = StatusCodes.Status500InternalServerError;

            var message = context.Exception.Message;

            var title = "Internal server error";

            switch (context.Exception)
            {
                case UnauthorizedAccessException:
                    status = StatusCodes.Status401Unauthorized;
                    title = "Unauthorized";
                    message = context.Exception.Message;
                    break;
            }

            // Customize the response (e.g., return a JSON error object)
            context.Result = new ObjectResult(new { title, message, status })
            {
                StatusCode = status,
                ContentTypes = { "application/json" }
            };

            context.ExceptionHandled = true;
        }
    }

}

As you can see it's basically the same structure of the other methods, we just modify the response that exists in the context. Now we let's see how to register our filter in our prgram.cs.

builder.Services.AddControllers(options =>
{
    options.Filters.Add<GlobalExceptionFilter>();
});
To summarize the differences between the three approaches:

- If you need application-wide exception handling with the ability to handle exceptions from all parts of your application (including other middleware), use middleware

- If you want to keep the exception handling logic related to HTTP requests and responses, and potentially have different behaviors for different endpoints, use the UseExceptionHandler extension.

- If your application primarily relies on MVC controllers and actions, and you want to tie the exception handling closely to those components, use filters.

I think that's all of it. Just remember to experiment with all approaches and see which one you prefer and don't forget to add a logger like Serilog or Apache Log4net. I will be making a post about setting up Log4net as a logger so make sure to stay tuned.

Comments

Popular posts

Google & Microsoft Token Validation Middleware in ASP.NET Applications

Publish and Consume Messages Using RabbitMQ as a Message Broker in 4 Simple Steps 🐰

Real Life Case Study: Synchronous Inter-Service Communication in Distributed Systems