ivanch.me/content/posts/error-handling-dotnet.md
2025-06-20 23:35:51 -03:00

3.2 KiB

title date draft summary
.NET - Proper API error handling 2024-06-20T20:00:06-03:00 false Because returning stack traces isn't secure.

The main idea behind having centralized error handling is that we can process any unhandled exception to:

  • Return formatted responses without revealing any internal functionality
  • Process issues so that they are properly logged on logs or other monitoring systems (like Sentry)
  • Make sure all errors have the same external behavior

For that, we will use a new middleware class:

public class ErrorResponse
{
    public string Message { get; set; }
}


public class ErrorHandlerMiddleware {
    private readonly RequestDelegate _next;
    private readonly ILogger<ErrorHandlerMiddleware> _logger;
    private readonly IHostEnvironment _env;

    public ErrorHandlerMiddleware(RequestDelegate next, ILogger<ErrorHandlerMiddleware> logger, IHostEnvironment env){
        _next = next;
        _logger = logger;
        _env = env;
    }

    public async Task Invoke(HttpContext httpContext)
    {
        // Attempts to execute the next action on the http chain
        // If it fails, we log the exception and trigger the HandleErrorAsync method
        try
        {
            await _next(httpContext);
        }
        catch(Exception ex)
        {
            _logger.LogError(ex, "An unhandled exception has occurred: {Message}", ex.Message);
            await HandleErrorAsync(httpContext, ex);
        }
    }

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

        ErrorResponse errorResponse;

        if (_env.IsDevelopment())
        {
            // In development, we want to see the full details for easier debugging.
            errorResponse = new ErrorResponse
            {
                Message = exception.ToString()
            };
        }
        else
        {
            // In production, we return a generic message to avoid leaking details.
            errorResponse = new ErrorResponse
            {
                Message = "An internal server error occurred. Please try again later."
            };
        }

        // We use the modern System.Text.Json for serialization via WriteAsJsonAsync
        await context.Response.WriteAsJsonAsync(errorResponse);
    }
}

We will also define a new Extension class to register this middleware:

public static class ErrorHandlerExtensions
{
    public static IApplicationBuilder UseErrorHandler(this IApplicationBuilder appBuilder)
    {
        return appBuilder.UseMiddleware<ErrorHandler>();
    }
}

And then we just configure it on the Configure() method at the startup:

public void Configure(IApplicationBuilder app)
{
    app.UseErrorHandler();
}

Now, when there's an issue on the API execution, the API will return something like this:

{
    "Message": "Internal Server Error"
}

Sources: