Best Practices for Exception Handling in .NET Core

Today in this post, we will see a few best practices for Exception Handling in .NET Core-based applications.

In our last post, we learned about the Aspect-oriented programming concept and understood how better cross-cutting concerns can be addressed.

Handling exceptions is another aspect that can be handled and streamlined for application development.

Exception handling for some of us is all about using try-catch blocks and throw statements in the application. Isn’t it?

Most of the time proper analysis or design approach never gets identified for exception handling.

Today in this article, we will cover the below aspects,

Best practices for exception handling

It’s really important to differentiate between different types of exceptions.

This differentiation will help developers to design and manage these exceptions carefully while developing the code.

This consideration can be applied for customer-facing UI or API or Backend jobs etc.

Let’s understand these exceptions and their types and why they are important.

Business Exceptions

  • Business exceptions are always different from technical exceptions/messages.

  • In business exceptions – Customers should experience consistency in responses to operations.

  • Business exceptions should not expose any technicalities of errors.

  • The higher end of your application which is API or UI (customer-facing) needs to provide graceful error messages.

  • Business exceptions should be as generic as possible and should talk about business operations. For example “Something went wrong, Please try again later “.

Technical Exceptions

Business exceptions are always mapped to Technical exceptions.

Whenever your application users see Business exceptions, your app will log more detailed Technical exceptions at the same time (mostly in the form of logging to some datastore – a file logging or SQL database loging or event viewer).

  • Technical exceptions are mapped with business exceptions and eventually should provide all technical details of exceptions.

  • Exception handling is an important aspect and an important cross-cutting concern.

  • Try to streamline and manage exception handling code to commonplace/modules to provide consistency.

  • Every exception needs to be handled properly.

  • Exception handling should not hide exception details.

  • Details of any Technical exceptions should include metadata like the origin of the error, file name, class name, method name, and code line number with the actual exception message.

  • Technical exceptions should be logged with the stack trace details as possible. Stack trace keeps track of the origin of issues and other details.

  • Do not swallow any exceptions without proper actions like logging etc.

  • Always throw exceptions and make the highest layer of your application swallow it. The top layer of the application could be UI or API.

  • Do not use empty catch blocks.

  • Use your custom exception details as much as possible else if using a built-in exception type be vary on business vs technical details logging.

  • For RESTFul API development apply HTTP Status based on the exception of the target resource.

Best Practices for Exception Handling in NET Core

Different Techniques of Handling Exception

  • Global Exception Handler – Explained in the current article.

Using Global Exception Handler in .NET Core

.NET Core provides an interface called “IExceptionHandlerFeaturewhich provides the ability to handle exception details within an API pipeline.

As a first step please create a static class ‘ExceptionHandler as shown below.

 public static class ExceptionHandler
    {
        public static void UseApiExceptionHandler(this IApplicationBuilder app, ILoggerFactory loggerFactory)
        {
            app.UseExceptionHandler(appError =>
            {
                appError.Run(async context =>
                {
                    context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
                    context.Response.ContentType = "application/json";
                    var contextFeature = context.Features.Get<IExceptionHandlerFeature>();
                    //if any exception then report it and log it
                    if (contextFeature != null)
                    {
                        //Technical Exception for troubleshooting
                        var logger = loggerFactory.CreateLogger("GlobalException");
                        logger.LogError($"Something went wrong: {contextFeature.Error}");

                        //Business exception - exit gracefully
                        await context.Response.WriteAsync(new ErrorDetails()
                        {
                            StatusCode = context.Response.StatusCode,
                            Message = "Something went wrongs.Please try again later"
                        }.ToString());
                    }
                });
            });
        }
    }

That’s all and you are all set to use this API in the pipeline by adding below the line of code in Startup.cs

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory logger)
        {

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseHsts();
                app.UseApiExceptionHandler(logger); //use global exception handlers
            }

           
            app.UseHttpsRedirection();
            app.UseMvc();
        }

With this approach, it’s important that other layers of application like BAL or Domain (DDD) or DAL should keep throwing an exception as a good practice.

These exceptions should be swallowed by the extreme layer of an application and depending on the application design, this top layer could be UI or API layer.

It’s all about a few best practices which developers can follow to take advantage of global exception handlers for effective exception handling.

Using the Global Middleware exception component

This is one of the most effective and preferred methods of handling exceptions globally.

I have talked about this technique in detail, please visit the below post for more details.

Using Exception filters in .NET Core

Using exception filters is also a good technique for handling exceptions.

The only limitation of this approach is that it’s effective for only API pipelines.

If there is an exception in other layers of your app apart from the API layer then this approach won’t be helpful.

Example – It won’t catch the exception that occurred in the middleware used in the app through the pipeline.

I have talked about this technique in detail, please visit the below post for more details.

Let’s look at the example of exception filters using .NET Core,

  • Once you have a sample API available, add the sample class ‘ExceptionInterceptor’ as below. Below shows examples of handling Synchronous and Asynchronous actions. Please choose the best approach suitable for your application.

  • The next step is how to handle this exception filter. Here we have a choice on the same. Just like action filters, we can control exception filters globally or controller or action level.

  • Sync: Here you need to derive your filter class from the IExceptionFilter interface and implement 2 methods OnException.

  • Async: Here you need to derive your filter class from the IExceptionFilter interface and implement 1 method OnExceptionAsync.

public class ServiceExceptionInterceptor : IAsyncExceptionFilter
    {
        public Task OnExceptionAsync(ExceptionContext context)
        {
            //Business exception-More generics for external world
            var error = new ErrorDetails()
            {
                StatusCode = 500,
                Message = "Something went wrong! Internal Server Error."
            };
            //Logs your technical exception with stack trace below
            // 

            context.Result = new JsonResult(error);
            return Task.CompletedTask;
        }
}

Let’s enable this exception filter globally in Startup.cs

 public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc(options=>options.Filters.Add(new ServiceExceptionInterceptor())).SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
        }

For ASP.NET Core 3.1 (IRouter routing and MVC template)

            services.AddMvc(options=> options.Filters.Add(new ServiceExceptionInterceptor())).SetCompatibilityVersion(CompatibilityVersion.Version_3_1);

For ASP.NET Core 3.1 (endpoint routing and new template)

public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers(options => options.Filters.Add(new ServiceExceptionInterceptor()));


..
..
         }

Generic exception code and message

Now let’s test the controller with some exceptions. Error response for most of the exceptions will be generic as below,

Fig REST API response for exception

For both above approaches, I have used a simple class for returning a response as below,

Best Practices for Exception Handling

Using Try-Catch-Throw – Best Practices

Understand the difference between throw vs throw ex vs throw new statements. Please visit the below post for more details.

Do you have any comments or ideas or any better suggestions to share?

Please sound off your comments below.

Happy Coding !!

Summary

In this article, we understood the differences between Business exceptions and Technical exceptions and also understood how bad exception handling can be misleading when trying to troubleshoot or find bugs. Good Exception handling should help a business user and the same time should save developers time finding technical issues.



Please bookmark this page and share it with your friends. Please Subscribe to the blog to receive notifications on freshly published(2024) best practices and guidelines for software design and development.