Error Handling and Exception Management in the API

Error Handling and Exception Management in the API
Error Handling and Exception Management in the API

Introduction

In any well-architected API, managing errors effectively is critical for ensuring stability, security, and ease of use. In the Contact Management Application, we follow clean architecture principles to implement structured error handling, ensuring the application can gracefully manage and respond to errors without exposing sensitive information to clients.

This article will cover:

  1. Why error handling is crucial in clean architecture.
  2. The implementation of global exception handling using middleware.
  3. Best practices for categorizing and managing exceptions.
  4. How to return consistent and meaningful error responses to clients.
  5. Code examples from the Contact Management Application.

Part of the Series

This article is part of a series on building and understanding the Contact Management Application, a sample project showcasing clean architecture principles. Below are the other articles in the series:

  1. Clean Architecture: Introduction to the Project Structure
  2. Clean Architecture: Implementing AutoMapper for DTO Mapping and Audit Logging
  3. Clean Architecture: Validating Inputs with FluentValidation
  4. Clean Architecture: Dependency Injection Setup Across Layers
  5. Clean Architecture: Handling Authorization and Role-Based Access Control (RBAC)
  6. Clean Architecture: Implementing Activity Logging with Custom Attributes
  7. Clean Architecture: Unit of Work Pattern and Its Role in Managing Transactions
  8. Clean Architecture: Using Dapper for Data Access and Repository Pattern
  9. Clean Architecture: Best Practices for Creating and Using DTOs in the API
  10. Clean Architecture: Error Handling and Exception Management in the API (You are here)
  11. Clean Architecture: Dockerizing the .NET Core API and MS SQL Server
  12. Clean Architecture: Seeding Initial Data Using Docker Compose and SQL Scripts

1. Why Error Handling is Crucial in Clean Architecture

Error handling in an API involves anticipating and responding to potential failures in a way that ensures:

  • Graceful degradation: The system remains stable, even in the face of errors.
  • Meaningful feedback: Clients receive useful error messages that help them understand what went wrong.
  • Security: Internal details of the application (such as stack traces) are not exposed to the client.
  • Maintainability: Developers can easily trace errors and troubleshoot based on structured logs and error reports.

In clean architecture, we separate concerns by isolating the domain logic from infrastructure and presentation layers. A solid error-handling mechanism fits perfectly within this architecture, ensuring that errors are captured and handled appropriately across all layers.


2. Global Exception Handling with Middleware

In ASP.NET Core, error handling can be centralized using custom middleware, allowing us to capture and manage all exceptions that occur in the application in one place.

In the Contact Management Application, we implement a custom ExceptionMiddleware to manage global exception handling. This middleware ensures that:

  • All exceptions are caught.
  • Meaningful error messages are returned to the client.
  • Logs are created for further investigation.

2.1 ExceptionMiddleware Implementation

Here is the implementation of the ExceptionMiddleware.cs:

using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using System;
using System.Net;
using System.Text.Json;
using System.Threading.Tasks;

namespace Contact.Api.Core.Middleware
{
    public class ExceptionMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly ILogger<ExceptionMiddleware> _logger;

        public ExceptionMiddleware(RequestDelegate next, ILogger<ExceptionMiddleware> logger)
        {
            _next = next;
            _logger = logger;
        }

        public async Task InvokeAsync(HttpContext context)
        {
            try
            {
                // Process the request
                await _next(context);
            }
            catch (Exception ex)
            {
                // Handle any unhandled exceptions
                _logger.LogError(ex, "An unhandled exception occurred");
                await HandleExceptionAsync(context, ex);
            }
        }

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

            var errorResponse = new ErrorResponse
            {
                StatusCode = context.Response.StatusCode,
                Message = "An unexpected error occurred. Please try again later.",
                DetailedMessage = exception.Message // Can be omitted in production to avoid exposing sensitive information
            };

            var errorJson = JsonSerializer.Serialize(errorResponse);

            return context.Response.WriteAsync(errorJson);
        }
    }

    public class ErrorResponse
    {
        public int StatusCode { get; set; }
        public string Message { get; set; }
        public string DetailedMessage { get; set; }
    }
}

In this middleware:

  • InvokeAsync() processes incoming requests and captures any unhandled exceptions.
  • HandleExceptionAsync() formats the exception into a standardized error response and sends it back to the client.
  • ErrorResponse defines the structure of the error message returned to the client.

2.2 Registering the Middleware

To ensure that all exceptions are caught, register the middleware in the Startup.cs or Program.cs file:

public class Startup
{
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        app.UseMiddleware<ExceptionMiddleware>();

        // Other middleware like routing, authentication, etc.
        app.UseRouting();
        app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
    }
}

With this setup, all unhandled exceptions in the application will pass through the ExceptionMiddleware.


3. Categorizing and Managing Exceptions

Not all errors are the same. Some represent client-side issues (e.g., bad input), while others are server-side issues. A good practice is to categorize exceptions into specific types, such as:

  • Validation exceptions: Errors related to invalid client inputs.
  • Not found exceptions: Errors when a resource (e.g., contact) is not found.
  • Authorization exceptions: Errors when users attempt to access unauthorized resources.
  • General exceptions: Server-side errors or unhandled exceptions.

By creating custom exceptions for these categories, we can handle them differently, returning appropriate status codes and messages.

3.1 Creating Custom Exception Classes




public class NotFoundException : Exception
{
    public NotFoundException(string message) : base(message) { }
}

public class ValidationException : Exception
{
    public ValidationException(string message) : base(message) { }
}

3.2 Handling Specific Exceptions in Middleware

We can modify the ExceptionMiddleware to handle specific exceptions differently:

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

    var errorResponse = new ErrorResponse();
    
    if (exception is NotFoundException)
    {
        context.Response.StatusCode = (int)HttpStatusCode.NotFound;
        errorResponse.Message = exception.Message;
    }
    else if (exception is ValidationException)
    {
        context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
        errorResponse.Message = exception.Message;
    }
    else
    {
        context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
        errorResponse.Message = "An unexpected error occurred. Please try again later.";
    }

    var errorJson = JsonSerializer.Serialize(errorResponse);

    return context.Response.WriteAsync(errorJson);
}

With this change, different exception types are handled and returned with appropriate status codes.


4. Returning Consistent Error Responses

Consistency is key when designing an API. Clients should expect errors in a specific format. In our Contact Management Application, all errors are returned as JSON objects using the ErrorResponse structure:

public class ErrorResponse
{
    public int StatusCode { get; set; }
    public string Message { get; set; }
    public string DetailedMessage { get; set; }
}

For example:

  • If a resource is not found, the client might receive the following error response:jsonCopy code{ "statusCode": 404, "message": "Contact not found", "detailedMessage": "" }
  • If a validation error occurs:jsonCopy code{ "statusCode": 400, "message": "Invalid email format", "detailedMessage": "" }
  • For internal server errors:jsonCopy code{ "statusCode": 500, "message": "An unexpected error occurred. Please try again later.", "detailedMessage": "" }

By standardizing the error response format, we make the API more predictable and easier for clients to handle.


5. Best Practices for Error Handling in APIs

Here are some best practices to follow when implementing error handling in your API:

  • Use meaningful status codes: Use appropriate HTTP status codes (e.g., 400 for bad requests, 404 for not found, 500 for server errors).
  • Avoid exposing sensitive data: Avoid exposing stack traces or internal details in error responses in production environments.
  • Log all exceptions: Always log exceptions for further investigation. Use structured logging to make logs easy to analyze.
  • Handle known exceptions gracefully: Anticipate common exceptions (e.g., validation errors, not found errors) and return meaningful messages to clients.
  • Fallback to generic error responses: For unexpected errors, return a generic error message (e.g., “An unexpected error occurred”) to avoid exposing sensitive information.

Conclusion

In this article, we explored how to implement error handling and exception management in the Contact Management Application using clean architecture principles. By centralizing error handling with custom middleware and categorizing exceptions, we ensure that the application handles errors gracefully and securely. Consistent error responses also make the API easier for clients to work with.

In the next article, we will discuss Dockerizing the .NET Core API and MS SQL Server.

For more details, check out the full project on GitHub:

GitHub Repository: Contact Management Application


Discover more from Nitin Singh

Subscribe to get the latest posts sent to your email.

7 Responses

  1. December 3, 2024

    […] Clean Architecture: Error Handling and Exception Management in the API […]

  2. December 3, 2024

    […] Clean Architecture: Error Handling and Exception Management in the API […]

  3. December 3, 2024

    […] Clean Architecture: Error Handling and Exception Management in the API […]

  4. December 3, 2024

    […] Clean Architecture: Error Handling and Exception Management in the API […]

  5. December 5, 2024

    […] Clean Architecture: Error Handling and Exception Management in the API […]

  6. December 16, 2024

    […] Clean Architecture: Error Handling and Exception Management in the API […]

  7. December 16, 2024

    […] Clean Architecture: Error Handling and Exception Management in the API […]

Leave a Reply