Introduction
Reliable exception handling is a cornerstone of robust software, regardless of scale or traffic. Poorly managed errors can lead to system instability or critical data loss. While ASP.NET Core offers various strategies for error management, this article explores the approach of Global Exception Handling using the IExceptionHandler interface. Centralizing error logic ensures consistent Problem Details (RFC 7807) responses, cleaner code, and a more maintainable architecture.
Exceptions Handling ways
In .NET, exceptions are specialized objects used to signal that an error has occurred during code execution. Every exception inherits from the baseSystem.Exception class. When a problem arises—such as attempting to access a missing file—the application "throws" an exception object to notify the system of the failure. This mechanism serves as a built-in alarm system, allowing the application to identify and respond to issues rather than simply crashing.
ASP.NET Core provides multiple strategies for managing application errors:
Traditional Try-Catch Blocks: The basic method of enclosing specific code blocks to capture and resolve local errors.
Default Exception Middleware: Leveraging built-in framework features like
UseExceptionHandlerandUseDeveloperExceptionPage.Custom Exception Middleware: Creating tailored middleware components to intercept and handle errors across the entire request lifecycle.
IExceptionHandler (Best Practice): The premier modern approach introduced in .NET 8. This interface is the preferred standard for implementing a clean, structured, and centralized error-handling system.
Lets discuss each one above.
1. Standard Try-Catch Block
The try-catch block is the most common approach for immediate error handling. The following example demonstrates this implementation:
[HttpGet]
public IActionResult GetProducts()
{
try
{
// Service call that may fail due to external factors
var products = _productService.FetchAll();
return Ok(products);
}
catch (Exception exception)
{
_logger.LogError(exception.Message);
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
This reflects a standard pattern where the FetchAll() method represents a service or data call prone to errors. When an exception occurs, the catch block intercepts it to log the error details and return a 500 Internal Server Error status code. While functional, repeating this logic across every controller can quickly lead to redundant code.
If an error occurs while theGetProducts() method is running, the following exception is triggered:
throw new Exception("An error occurred while retrieving data.");
While this basic approach is straightforward, it significantly increases the total lines of code. While suitable for small Proof of Concept (POC) projects, manually inserting try-catch blocks into every controller action and service method is repetitive and difficult to maintain in larger systems.
A more effective strategy involves handling all exceptions in a single, central location. The following sections explore two approaches that improve the error-handling architecture by isolating all logic into a dedicated area. This results in a cleaner codebase and a more controlled application by removing the need for redundant error-handling code.
2. Exception Handling Middleware in .NET - UseExceptionHandler
To simplify development, ASP.NET Core provides the built-in ExceptionHandler Middleware. When configured in the Program.cs file, this middleware is added to the request pipeline to catch unhandled exceptions across the entire application. It offers a direct and efficient way to centralize error management.
The following example demonstrates how to configure the UseExceptionHandler middleware. Open the Program.cs file and apply the following configuration:
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
var app = builder.Build();
// Configure the built-in exception handling middleware
app.UseExceptionHandler("/error-handler");
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
This configuration provides a foundational setup for the ExceptionHandler Middleware. Whenever the application pipeline detects an unhandled exception, execution control is automatically transferred to this middleware. It then intercepts the error and returns a customized response to the client, ensuring the application remains stable and provides informative feedback.
3. Custom Exceptions
It is both cleaner and more effective to categorize specific error types. This allows the application to respond uniquely to different failure scenarios later in the development process. Creating custom exception classes ensures that the application throws meaningful errors that are easier to diagnose.
To begin, create a new folder named Exceptions and add a class named ApplicationBaseException. This class must inherit from the baseException class. The implementation for this custom base exception is as follows:
namespace GlobalExceptionHandler.Domain.Exceptions
{
public class GlobalAppException : Exception
{
public HttpStatusCode ResultStatusCode { get; }
public GlobalAppException(string errorMessage, HttpStatusCode code = HttpStatusCode.InternalServerError)
: base(errorMessage)
{
ResultStatusCode = code;
}
}
}
This GlobalAppException serves as the foundation for creating more specific error types. Inheriting from a shared base class is a cleaner architectural pattern for designing exception systems. For instance, a RecordNotFoundException can be created by inheriting from this base class.
public class RecordNotFoundException : GlobalAppException
{
public RecordNotFoundException(Guid recordId)
: base($"The record with ID {recordId} was not found.", HttpStatusCode.NotFound)
{
}
}
By using this approach, a service class can simply throw a RecordNotFoundException if a database or cache lookup fails. This provides immediate clarity in error logs regarding the nature of the issue and the specific identifier involved, eliminating the need to sift through complex stack traces.
The custom exception is utilized as follows:
throw new RecordNotFoundException(requestedProduct.Id);
4. Recommended exception handling in .NET 8: IExceptionHandler
Introduced in .NET 8, theIExceptionHandler interface is the current recommended standard for global error management. It is utilized internally by the framework and offers a more structured approach than traditional middleware.
The interface requires implementing a single method, TryHandleAsync, which processes the HTTP context and the specific exception. A major advantage is that it integrates directly with the existing UseExceptionHandler middleware, eliminating the need for a custom middleware class.
This approach allows for modular, maintainable code by enabling separate handling logic for different error types. The following example demonstrates how to implement IExceptionHandler:
The AppExceptionHandler class implements the IExceptionHandler interface to centralize error logic. The TryHandleAsync method processes the exception and returns true to signal that the error has been successfully handled. Returning false would allow the error to pass to the next handler in a chain.
public class AppExceptionHandler(ILogger<AppExceptionHandler> logger) : IExceptionHandler
{
public async ValueTask<bool> TryHandleAsync(
HttpContext context,
Exception exception,
CancellationToken cancellationToken)
{
var details = new ProblemDetails
{
Instance = context.Request.Path,
Status = StatusCodes.Status500InternalServerError
};
if (exception is GlobalAppException appEx)
{
context.Response.StatusCode = (int)appEx.ResultStatusCode;
details.Title = appEx.Message;
details.Status = (int)appEx.ResultStatusCode;
}
else
{
details.Title = "An unexpected error occurred.";
}
logger.LogError(exception, "{ErrorTitle}", details.Title);
await context.Response.WriteAsJsonAsync(details, cancellationToken);
return true;
}
}
To activate this handler, register it in Program.cs. This setup links the implementation with the built-in Problem Details services and the standard exception middleware.
builder.Services.AddExceptionHandler<AppExceptionHandler>();
builder.Services.AddProblemDetails();
// ... other configurations
var app = builder.Build();
app.UseExceptionHandler(); // Middleware to use the registered handlers
Conclusion
In this article we have seen various strategies for managing errors in ASP.NET Core. TheIExceptionHandler interface remains the recommended standard for modern applications, offering superior control and readability. Implementing this centralized approach ensures a cleaner, more maintainable, and resilient codebase. Hope you will find this useful.

Join the conversation! Your thoughts help the community grow.