Handling Errors with IExceptionHandler in ASP.NET Core 8.0

Anton Antonov
4 min readJan 19, 2024

--

Let’s explore how you might implement error handling using the IExceptionHandler introduced in .NET Core 8.0. It follows similar patterns to previous error handling approaches in ASP.NET Core, but adds an additional option to inject your custom exception-handling logic into the exception handling middleware.

I have previously described my approach to processing unhandled exceptions in ASP.NET Core Web API in an earlier article. There, you can find an example of a handler that meets the most common requirements for application monitoring and support. Here, I want to update this approach to use the new IExceptionHandler interface.

The exception handling middleware efficiently manages several key aspects: it handles cases where the client closes the request or when the response has already started. It also clears the HTTP context, sets an appropriate HTTP status code, and logs the error along with diagnostic data.

Invoke UseExceptionHandler() to configure the ASP.NET Core pipeline to use the middleware for handling exceptions. It is recommended to place this call at the start of the pipeline to ensure it catches any exceptions that arise during request processing, as illustrated in the code below.

public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddProblemDetails();
services.AddControllers();
}

public void Configure(IApplicationBuilder app)
{
app.UseExceptionHandler(); // Should be always in first place

app.UseRouting();
app.UseCors();

app.UseAuthentication();
app.UseAuthorization();

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

Additionally, .NET Core requires us to register the ProblemDetails service then the middleware will also generate ProblemDetails standardized responses as per RFC 7807 specification. However, a notable concern is that it may inadvertently disclose sensitive internal information to Web API clients, such as stack traces and exception data, which could pose security risks. In development environments, though, detailed exception information can be invaluable. Therefore, I recommend using the Exception.Data property to log user-defined information. This technique, particularly useful for debugging, was detailed in my first article on exception handling.

Consequently, it is advisable to implement your own custom exception-handling logic that ensures safety, meets other requirements, and returns responses in a suitable format. The IExceptionHandler interface consists only of the TryHandleAsync method. This method should return true if the exception is handled. If it returns false, the exception is passed on to the next handler or, if no other handlers are available, the default handling logic is applied.

Let’s create a GlobalExceptionHandler class that inherits from the IExceptionHandler.

public class GlobalExceptionHandler(IHostEnvironment env, ILogger<GlobalExceptionHandler> logger)
: IExceptionHandler
{
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) }
};

public async ValueTask<bool> TryHandleAsync(HttpContext context, Exception exception,
CancellationToken cancellationToken)
{
exception.AddErrorCode();

//If your logger logs DiagnosticsTelemetry, you should remove the string below to avoid the exception being logged twice.
logger.LogError(exception, exception is YourAppException ? exception.Message : UnhandledExceptionMsg);

var problemDetails = CreateProblemDetails(context, exception);
var json = ToJson(problemDetails);

const string contentType = "application/problem+json";
context.Response.ContentType = contentType;
await context.Response.WriteAsync(json, cancellationToken);

return true;
}

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;
}
}

Then register this class, as shown in the following code.

services.AddExceptionHandler<GlobalExceptionHandler>();

See the example below for a sample response in Dev or QA environments.

{
"title": "Internal Server Error",
"status": 500,
"detail": "TestApp.Exceptions.YourAppException: Unable to get order info\r\n ---> System.Exception: Some trouble with connection :)",
"errorCode": "523f53f52",
"traceId": "0HN0OCHPUOMUU:00000001",
"data": {
"userName": "Anton Antonov",
"id": 999,
"invoice": {
"id": 111111,
"date": "2024-01-19T01:31:30.5287118+05:00",
"status": "unpaid"
},
"errorCode": "523f53f52"
}
}

How it looks in Production environment.

{
"title": "Internal Server Error",
"status": 500,
"errorCode": "523f53f52"
}

I’m against using exceptions to manage application flow, for several key reasons. Firstly, it goes against their intended purpose: handling unexpected errors, not controlling regular program logic. This can lead to confusing code, difficult for other developers to understand, and increasing maintenance challenges. Secondly, exceptions are resource-intensive, involving capturing a stack trace, which can negatively impact performance, especially in high-volume or performance-sensitive applications. Lastly, frequent use of exceptions for normal flow can obscure genuine error conditions, making debugging more complex.

Therefore, I believe it’s best to have a single global error handler for unexpected errors. The exception handling middleware, however, does provide the flexibility to inject additional handlers if necessary. Let’s create an example class, named ValidationExceptionHandler.

public class ValidationExceptionHandler : IExceptionHandler
{
public async ValueTask<bool> TryHandleAsync(HttpContext context, Exception exception,
CancellationToken cancellationToken)
{
if (exception is ValidationException validationException)
{
context.Response.StatusCode = StatusCodes.Status400BadRequest;
await context.Response.WriteAsJsonAsync(validationException.ValidationResult, cancellationToken);

return true;
}

return false;
}
}

Then register this class and position it above the GlobalExceptionHandler, as shown in the following code example.

services.AddExceptionHandler<ValidationExceptionHandler>();
services.AddExceptionHandler<GlobalExceptionHandler>();

See the example below for a sample response.

{
"errorMessage": "Date isn't in the correct format"
}

I hope this approach will help you in supporting your applications. If you have any suggestions for enhancing this method, please feel free to share them 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.

--

--

Anton Antonov

I have more than 14 years of experience. I started with .NET Framework 3.5. Now I’m an expert in C#, T-SQL, HTML, CSS, TypeScript, Angular, and Docker