The exception handling features help us deal with unforeseen errors that could appear in our code.  To handle exceptions we can use the try-catch block in our code as well as finally keyword to clean up resources afterward.

Even though there is nothing wrong with the try-catch blocks in our Actions in Web API project, we can extract all the exception-handling logic into a single centralized place. Doing that makes our actions more readable and the error handling process more maintainable. If we want to make our actions more readable and maintainable, we can implement Action Filters. We won’t talk about action filters in this article but we strongly recommend reading our post Action Filters in .NET Core.

In this article, we are going to handle errors by using a try-catch block first and then rewrite our code by using built-in middleware and our custom middleware for global error handling to demonstrate the benefits of this approach. We are going to use an ASP.NET Core Web API project to explain these features and if you want to learn more about it (which we strongly recommend), you can read our ASP.NET Core Web API Tutorial.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

VIDEO: Global Error Handling in ASP.NET Core Web API video.


To download the source code for our starting project, you can visit the Global error handling start project.

For the finished project refer to Global error handling end project.

Let’s start.

Error Handling With Try-Catch Block

To start with this example, let’s open the Values Controller from the starting project (Global-Error-Handling-Start project). In this project, we can find a single Get() method and an injected Logger service.

It is a common practice to include the log messages while handling errors, therefore we have created the LoggerManager service. It logs all the messages to the C drive, but you can change that by modifying the path in the nlog.config file. For more information about how to use NLog in .NET Core, you can visit Logging with NLog.

Now, let’s modify our action method to return a result and log some messages:

[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
    private ILoggerManager _logger;
    public ValuesController(ILoggerManager logger)
    {
        _logger = logger;
    }
    [HttpGet]
    public IActionResult Get()
    {
        try
        {
            _logger.LogInfo("Fetching all the Students from the storage");
            var students = DataManager.GetAllStudents(); //simulation for the data base access
            _logger.LogInfo($"Returning {students.Count} students.");
            return Ok(students);
        }
        catch (Exception ex)
        {
            _logger.LogError($"Something went wrong: {ex}");
            return StatusCode(500, "Internal server error");
        }
    }
}

When we send a request at this endpoint, we will get this result:

Basic request - Global Error Handling

And the log messages:

log basic request - Global Error Handling

We see that everything is working as expected.

Now let’s modify our code, right below the GetAllStudents() method call, to force an exception:

throw new Exception("Exception while fetching all the students from the storage.");

Now, if we send a request:

try catche error - Global Error Handling

And the log messages:

log try catch error

So, this works just fine. But the downside of this approach is that we need to repeat our try-catch blocks in all the actions in which we want to catch unhandled exceptions. Well, there is a better approach to do that.

Handling Errors Globally With the Built-In Middleware

The UseExceptionHandler() middleware is a built-in middleware that we can use to handle exceptions in our ASP.NET Core Web API application. So, let’s dive into the code to see this middleware in action.

First, we are going to add a new class ErrorDetails in the Models folder:

public class ErrorDetails
{
    public int StatusCode { get; set; }
    public string Message { get; set; }
    public override string ToString()
    {
        return JsonSerializer.Serialize(this);
    }
}

We are going to use this class for the details of our error message.

To continue, let’s create a new folder Extensions and a new static class ExceptionMiddlewareExtensions inside it.

Now, we need to modify it:

public static class ExceptionMiddlewareExtensions
{
    public static void ConfigureExceptionHandler(this IApplicationBuilder app, ILoggerManager logger)
    {
        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 (contextFeature != null)
                {
                    logger.LogError($"Something went wrong: {contextFeature.Error}");
                    await context.Response.WriteAsync(new ErrorDetails()
                    {
                        StatusCode = context.Response.StatusCode,
                        Message = "Internal Server Error."
                    }.ToString());
                }
            });
        });
    }
}

In the code above, we’ve created an extension method in which we’ve registered the UseExceptionHandler middleware. Then, we populate the status code and the content type of our response, log the error message and finally return the response with the custom-created object.

To be able to use this extension method, let’s modify the Configure method inside the Startup class for .NET 5 project:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerManager logger) 
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.ConfigureExceptionHandler(logger);

    app.UseHttpsRedirection();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

Or if you are using .NET 6 and above:

var app = builder.Build();

var logger = app.Services.GetRequiredService<ILoggerManager>();
app.ConfigureExceptionHandler(logger);

Finally, let’s remove the try-catch block from our code:

public IActionResult Get()
{
    _logger.LogInfo("Fetching all the Students from the storage");

     var students = DataManager.GetAllStudents(); //simulation for the data base access

     throw new Exception("Exception while fetching all the students from the storage.");

     _logger.LogInfo($"Returning {students.Count} students.");

     return Ok(students);
}

And there you go. Our action method is much cleaner now and what’s more important we can reuse this functionality to write more readable actions in the future.

So let’s inspect the result:

Global Handler Middleware

And the log messages:

log global handler middleware

Excellent.

Now, we are going to use custom middleware for global error handling.

Handling Errors Globally With the Custom Middleware

Let’s create a new folder named CustomExceptionMiddleware and a class ExceptionMiddleware.cs inside it.

We are going to modify that class:

public class ExceptionMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILoggerManager _logger;

    public ExceptionMiddleware(RequestDelegate next, ILoggerManager logger)
    {
        _logger = logger;
        _next = next;
    }

    public async Task InvokeAsync(HttpContext httpContext)
    {
        try
        {
            await _next(httpContext);
        }
        catch (Exception ex)
        {
            _logger.LogError($"Something went wrong: {ex}");
            await HandleExceptionAsync(httpContext, ex);
        }
    }

    private async Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        context.Response.ContentType = "application/json";
        context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;

        await context.Response.WriteAsync(new ErrorDetails()
        {
            StatusCode = context.Response.StatusCode,
            Message = "Internal Server Error from the custom middleware."
        }.ToString());
    }
}

The first thing we need to do is to register our IloggerManager service and RequestDelegate through the dependency injection. The _next parameter of RequestDeleagate type is a function delegate that can process our HTTP requests.

After the registration process, we create the InvokeAsync() method. RequestDelegate can’t process requests without it.

If everything goes well, the _next delegate should process the request, and the Get action from our controller should generate a successful response. But if a request is unsuccessful (and it is, because we are forcing an exception), our middleware will trigger the catch block and call the HandleExceptionAsync method.

In that method, we just set up the response status code and content type and return a response.

Now let’s modify our ExceptionMiddlewareExtensions class with another static method:

public static void ConfigureCustomExceptionMiddleware(this IApplicationBuilder app)
{
    app.UseMiddleware<ExceptionMiddleware>();
}

In .NET 6 and above, we have to extend the WebApplication type:

public static void ConfigureCustomExceptionMiddleware(this WebApplication app) 
{ 
    app.UseMiddleware<ExceptionMiddleware>(); 
}

Finally, let’s use this method in the Configure method in the Startup class:

//app.ConfigureExceptionHandler(logger);
app.ConfigureCustomExceptionMiddleware();

Now let’s inspect the result again:

custom handler middleware

There we go. Our custom middleware is implemented in a couple of steps.

Customizing Error Messages

If you want, you can always customize your error messages from the error handler. There are different ways of doing that, but we are going to show you the basic two ways.

First of all, we can assume that the AccessViolationException is thrown from our action:

[HttpGet]
public IActionResult Get()
{
    _logger.LogInfo("Fetching all the Students from the storage");

    var students = DataManager.GetAllStudents(); //simulation for the data base access

    throw new AccessViolationException("Violation Exception while accessing the resource.");

    _logger.LogInfo($"Returning {students.Count} students.");

    return Ok(students);
}

Now, what we can do is modify the InvokeAsync method inside the ExceptionMiddleware.cs class by adding a specific exception checking in the additional catch block:

public async Task InvokeAsync(HttpContext httpContext)
{
    try
    {
        await _next(httpContext);
    }
    catch (AccessViolationException avEx)
    {
        _logger.LogError($"A new violation exception has been thrown: {avEx}");
        await HandleExceptionAsync(httpContext, avEx);
    }
    catch (Exception ex)
    {
        _logger.LogError($"Something went wrong: {ex}");
        await HandleExceptionAsync(httpContext, ex);
    }
}

Now if we send another request with Postman, we are going to see in the log file that the AccessViolationException message is logged. Of course, our specific exception check must be placed before the global catch block.

With this solution, we are logging specific messages for the specific exceptions, and that can help us, as developers, a lot when we publish our application. But if we want to send a different message for a specific error, we can also modify the HandleExceptionAsync method in the same class:

private async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
    context.Response.ContentType = "application/json";
    context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;

    var message = exception switch
    {
        AccessViolationException =>  "Access violation error from the custom middleware",
        _ => "Internal Server Error from the custom middleware."
    };

    await context.Response.WriteAsync(new ErrorDetails()
    {
        StatusCode = context.Response.StatusCode,
        Message = message
    }.ToString());
}

Here, we are using a switch expression pattern matching to check the type of our exception and assign the right message to the message variable. Then, we just use that variable in the WriteAsync method.

Now if we test this, we will get a log message with the Access violation message, and our response will have a new message as well:

{
    "StatusCode": 500,
    "Message": "Access violation error from the custom middleware"
}

One thing to mention here. We are using the 500 status code for all the responses from the exception middleware, and that is something we believe should be done. After all, we are handling exceptions and these exceptions should be marked with a 500 status code. But this doesn’t have to be the case all the time. For example, if you have a service layer and you want to propagate responses from the service methods as custom exceptions and catch them inside the global exception handler, you may want to choose a more appropriate status code for the response. You can read more about this technique in the article about the Onion Architecture. It depends on your project organization.

Using the IExceptionHandler Interface from .NET 8

IExceptionHandler is an interface that we can use to handle exceptions in ASP.NET Core applications. It defines an interface that we can implement to handle exceptions globally. This allows us to write custom logic for handling individual exceptions or groups of exceptions based on their type, in turn providing tailored responses, error messages as well as logging. 

Since we already have an article on this topic, feel free to read it here. You will find all the information you need to use this interface, which will improve the handling logic as well.

Conclusion

That was awesome.

We have learned, how to handle errors in a more sophisticated way and cleaner as well. The code is much more readable and our exception handling logic is now reusable for the entire project.

Thank you for reading this article. We hope you have learned new useful things.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!