How processing unhandled exceptions in ASP.NET Core Web API
Hi! I’m Anton Antonov, a full stack developer. I’m going to tell you how I process unhandled exceptions in ASP.NET Core Web API. There are a lot of articles about the topic. I have tried to describe my approach, share my experience and ideas, and give you examples of error handlers that meet the most common requirements.
To provide a consistent, simple way of processing exceptions don’t throw exceptions in cases where you can detect an error, I recommend returning an appropriate error response. How in code bellow.
[HttpGet("{id:int}")]
public async Task<ActionResult<Order>> Get(int id, CancellationToken cancellationToken)
{
var order = await _ordersService.Get(id, cancellationToken);
if (order == null)
{
return NotFound();
}
return Ok(order);
}
Don’t use errors to manage application flow. Using exceptions reduces the performance, makes your code less readable, breaks the flow, and leads to a lot of extra work to handle exceptions appropriately.
Also, you should avoid the API state when throwing an exception, and sending the 500 error is the only way to response. If it happens it should be a reason to refactor your API design and use cases. So, send the 500 error only in exceptional unhandled cases; like database troubles, system exceptions, etc.
There are a few ways to process unhandled exceptions in ASP.NET Core. They are Exception Filters, Exception handler lambda, and Middleware. I prefer the last one. Unlike the filters, Middleware catches exceptions from controller constructors, other filters and handlers, routing, etc.
Implement IMiddleware interface, and register this class in the Startup.cs how in code below. The Exception Handler should be the first in the pipeline to catch any exception from request processing.
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// Middlewares
services.AddTransient<ErrorHandlerMiddleware>();
services.AddTransient<YourCustomMiddleware>();
services.AddControllers();
}
public void Configure(IApplicationBuilder app)
{
app.UseMiddleware<ErrorHandlerMiddleware>(); // Should be always in the first place
app.UseRouting();
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
app.UseMiddleware<YourCustomMiddleware>();
app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
}
}
If an exception is unhandled API clients will receive the “Unknown Error”. The simplest error handler should catch the exception, log it, and send the “Internal Server Error” status. The following code adds a C# class that does this.
public class ErrorHandlerMiddleware : IMiddleware
{
private readonly ILogger<ErrorHandlerMiddleware> _logger;
public ErrorHandlerMiddleware(ILogger<ErrorHandlerMiddleware> logger)
{
_logger = logger;
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
try
{
await next(context);
}
catch (Exception exception)
{
const string message = "An unhandled exception has occurred while executing the request.";
_logger.LogError(exception, message);
context.Response.Clear();
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
}
}
}
In my view, you don’t have to check the context.Response.HasStarted property. .NET itself handles it pretty well throwing InvalidOperationException with an appropriate message. How it looks in the console in our example.
Is it enough? In my projects usually, I have more requirements for error handling. Here they are:
- Log more details about exceptions. Don’t define details in an exception message. About how to use Exception.Data property to log user-defined information you can read in my previous article.
- Don’t send secret internal information with the error to Web API clients like stack trace, exception data, etc.
- Don’t handle TaskCanceledException as the Internal Server Error when the reason is closing request by the client so the more appropriate HTTP response is 499 in this case.
- Use JSON as the more appropriate web format for error handling on the client side.
- Error messages should be localized so there is no point showing users an exception message. It should be something not specific like: “Oops! Something went wrong.” that can be localized on the client side.
- Send with the error some error code that helps users contact support about the issue.
- Use a monitoring platform for storing, analyzing logs, finding, and aggregating issues by error data including the error code. It creates possibilities for the automatization of your app support.
A more complex error handler that meets these requirements.
public class ErrorHandlerMiddleware : IMiddleware
{
private const string UnhandledExceptionMsg = "An unhandled exception has occurred while executing the request.";
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
{
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }
};
private readonly IWebHostEnvironment _env;
private readonly ILogger<ErrorHandlerMiddleware> _logger;
public ErrorHandlerMiddleware(IWebHostEnvironment env, ILogger<ErrorHandlerMiddleware> logger)
{
_env = env;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
try
{
await next(context);
}
catch (Exception exception) when (context.RequestAborted.IsCancellationRequested)
{
const string message = "Request was cancelled";
_logger.LogDebug(exception, message);
context.Response.Clear();
context.Response.StatusCode = 499; //Client Closed Request
}
catch (Exception exception)
{
exception.AddErrorCode();
_logger.LogError(exception, exception is YourAppException ? exception.Message : UnhandledExceptionMsg);
const string contentType = "application/problem+json";
context.Response.Clear();
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
context.Response.ContentType = contentType;
var problemDetails = CreateProblemDetails(context, exception);
var json = ToJson(problemDetails);
await context.Response.WriteAsync(json);
}
}
private ProblemDetails CreateProblemDetails(in HttpContext context, in Exception exception)
{
var errorCode = exception.GetErrorCode();
var statusCode = context.Response.StatusCode;
var reasonPhrase = ReasonPhrases.GetReasonPhrase(statusCode);
if (string.IsNullOrEmpty(reasonPhrase))
{
reasonPhrase = UnhandledExceptionMsg;
}
var problemDetails = new ProblemDetails
{
Status = statusCode,
Title = reasonPhrase,
Extensions =
{
[nameof(errorCode)] = errorCode
}
};
if (!_env.IsDevelopmentOrQA())
{
return problemDetails;
}
problemDetails.Detail = exception.ToString();
problemDetails.Extensions["traceId"] = context.TraceIdentifier;
problemDetails.Extensions["data"] = exception.Data;
return problemDetails;
}
private string ToJson(in ProblemDetails problemDetails)
{
try
{
return JsonSerializer.Serialize(problemDetails, SerializerOptions);
}
catch (Exception ex)
{
const string msg = "An exception has occurred while serializing error to JSON";
_logger.LogError(ex, msg);
}
return string.Empty;
}
}
The error code could just be a generated GUID. I suggest using a hash code of the exception as the error code to send users the same error code on a similar issue. You can use any hash algorithm that produces a short error code. I prefer commonly available SHA-1 then truncate the result to the desired length, it should be enough to create a quite unique error code. The following code adds an extension to the Exception class that does this.
private const string ErrorCodeKey = "errorCode";
public static Exception AddErrorCode(this Exception exception)
{
using var sha1 = SHA1.Create();
var hash = sha1.ComputeHash(Encoding.UTF8.GetBytes(exception.Message));
var errorCode = string.Concat(hash[..5].Select(b => b.ToString("x")));
exception.Data[ErrorCodeKey] = errorCode;
return exception;
}
public static string GetErrorCode(this Exception exception)
{
return (string)exception.Data[ErrorCodeKey];
}
The example of a simple popup about an error on the client-side.
I hope this approach will help you to support apps. If you have any ideas on how to improve the approach, please let me know in the comments.
If you find my work valuable, please consider sponsoring me on GitHub. Your support will help me create more content and share more code.
Here’s the link to sponsor me: My GitHub Profile
Thank you for considering! Your support means a lot to me.
P.S.: .NET Core 8.0 introduces the new IExceptionHandler interface. For those who have already upgraded to .NET 8.0 or are considering the transition, I highly recommend adopting this interface. For a comprehensive understanding of the changes, refer to the article titled “Error Handling with IExceptionHandler in ASP.NET Core 8.0”.