Section 5 of 5

Exception Handling Middleware

đŸŽ¯ What You'll Learn

  • Built-in exception handling middleware
  • Developer Exception Page
  • UseExceptionHandler middleware
  • Custom exception handling middleware
  • Logging exceptions
  • Returning JSON error responses
  • Problem Details (RFC 7807)
  • Production-ready error handling

Why Exception Handling Matters

Without proper exception handling:

  • Users see ugly error pages
  • Sensitive information leaks (stack traces, connection strings)
  • Errors go unlogged
  • APIs return HTML instead of JSON
âš ī¸ Security Risk

Never expose detailed error information in production! Stack traces can reveal:

  • File paths and directory structure
  • Database connection strings
  • Third-party library versions
  • Business logic details

1. Developer Exception Page

In development, use the Developer Exception Page for detailed error information:

Development Only C#
if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler("/error");
}

What It Shows

  • Full stack trace
  • Exception message
  • Request headers
  • Query string
  • Cookies
  • Routing data
â„šī¸ ASP.NET Core 6+

In ASP.NET Core 6+, UseDeveloperExceptionPage() is added automatically in development. You don't need to call it explicitly.

2. UseExceptionHandler Middleware

In production, use UseExceptionHandler to handle errors gracefully:

Basic Usage C#
app.UseExceptionHandler("/error");

// Define error endpoint
app.MapGet("/error", () => Results.Problem());

With Custom Error Page

Controllers/ErrorController.cs C#
[ApiController]
public class ErrorController : ControllerBase
{
    [Route("/error")]
    [ApiExplorerSettings(IgnoreApi = true)]
    public IActionResult HandleError()
    {
        return Problem(
            title: "An error occurred",
            statusCode: 500);
    }
}

3. Custom Exception Handling Middleware

For full control, create custom exception handling middleware:

Middleware/GlobalExceptionMiddleware.cs C#
public class GlobalExceptionMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<GlobalExceptionMiddleware> _logger;
    private readonly IHostEnvironment _environment;

    public GlobalExceptionMiddleware(
        RequestDelegate next,
        ILogger<GlobalExceptionMiddleware> logger,
        IHostEnvironment environment)
    {
        _next = next;
        _logger = logger;
        _environment = environment;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "An unhandled exception occurred");
            await HandleExceptionAsync(context, ex);
        }
    }

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

        var response = new
        {
            error = "An error occurred while processing your request",
            details = _environment.IsDevelopment() ? exception.Message : null,
            stackTrace = _environment.IsDevelopment() ? exception.StackTrace : null
        };

        await context.Response.WriteAsJsonAsync(response);
    }
}

Registration

Program.cs C#
app.UseMiddleware<GlobalExceptionMiddleware>();

4. Problem Details (RFC 7807)

Use the Problem Details standard for consistent API error responses:

Problem Details Response JSON
{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "traceId": "00-abc123...",
  "errors": {
    "Name": ["The Name field is required."]
  }
}

Enable Problem Details

Program.cs C#
builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseExceptionHandler();
app.UseStatusCodePages();

Custom Problem Details

Middleware/ProblemDetailsExceptionMiddleware.cs C#
public class ProblemDetailsExceptionMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ProblemDetailsExceptionMiddleware> _logger;

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

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            await HandleExceptionAsync(context, ex);
        }
    }

    private async Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        _logger.LogError(exception, "An error occurred: {Message}", exception.Message);

        var (statusCode, title) = exception switch
        {
            ArgumentException => (400, "Bad Request"),
            UnauthorizedAccessException => (401, "Unauthorized"),
            KeyNotFoundException => (404, "Not Found"),
            _ => (500, "Internal Server Error")
        };

        var problemDetails = new ProblemDetails
        {
            Status = statusCode,
            Title = title,
            Detail = exception.Message,
            Instance = context.Request.Path
        };

        problemDetails.Extensions["traceId"] = context.TraceIdentifier;

        context.Response.StatusCode = statusCode;
        context.Response.ContentType = "application/problem+json";

        await context.Response.WriteAsJsonAsync(problemDetails);
    }
}

5. Logging Exceptions

Comprehensive Logging C#
private async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
    // Log with structured data
    _logger.LogError(
        exception,
        "Unhandled exception: {ExceptionType} | Path: {Path} | Method: {Method} | User: {User}",
        exception.GetType().Name,
        context.Request.Path,
        context.Request.Method,
        context.User.Identity?.Name ?? "Anonymous");

    // Return error response...
}

Complete InvenTrack Example

Middleware/InvenTrackExceptionMiddleware.cs C#
public class InvenTrackExceptionMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<InvenTrackExceptionMiddleware> _logger;
    private readonly IHostEnvironment _environment;

    public InvenTrackExceptionMiddleware(
        RequestDelegate next,
        ILogger<InvenTrackExceptionMiddleware> logger,
        IHostEnvironment environment)
    {
        _next = next;
        _logger = logger;
        _environment = environment;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            await HandleExceptionAsync(context, ex);
        }
    }

    private async Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        // Determine status code and title based on exception type
        var (statusCode, title, logLevel) = exception switch
        {
            ArgumentException => (400, "Bad Request", LogLevel.Warning),
            UnauthorizedAccessException => (401, "Unauthorized", LogLevel.Warning),
            KeyNotFoundException => (404, "Not Found", LogLevel.Warning),
            InvalidOperationException => (409, "Conflict", LogLevel.Warning),
            _ => (500, "Internal Server Error", LogLevel.Error)
        };

        // Log the exception
        _logger.Log(
            logLevel,
            exception,
            "[InvenTrack] {ExceptionType} | {Method} {Path} | User: {User} | TraceId: {TraceId}",
            exception.GetType().Name,
            context.Request.Method,
            context.Request.Path,
            context.User.Identity?.Name ?? "Anonymous",
            context.TraceIdentifier);

        // Build Problem Details response
        var problemDetails = new ProblemDetails
        {
            Status = statusCode,
            Title = title,
            Type = $"https://httpstatuses.com/{statusCode}",
            Instance = context.Request.Path
        };

        // Add detail only in development
        if (_environment.IsDevelopment())
        {
            problemDetails.Detail = exception.Message;
            problemDetails.Extensions["stackTrace"] = exception.StackTrace;
        }
        else
        {
            problemDetails.Detail = "An error occurred while processing your request.";
        }

        // Add trace ID
        problemDetails.Extensions["traceId"] = context.TraceIdentifier;

        // Add correlation ID if available
        if (context.Items.TryGetValue("X-Correlation-Id", out var correlationId))
        {
            problemDetails.Extensions["correlationId"] = correlationId;
        }

        // Set response
        context.Response.StatusCode = statusCode;
        context.Response.ContentType = "application/problem+json";

        await context.Response.WriteAsJsonAsync(problemDetails);
    }
}

Extension Method

Extensions/MiddlewareExtensions.cs C#
public static IApplicationBuilder UseInvenTrackExceptionHandler(
    this IApplicationBuilder app)
{
    return app.UseMiddleware<InvenTrackExceptionMiddleware>();
}

Usage in Program.cs

Program.cs C#
var app = builder.Build();

// Exception handling (FIRST!)
if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseInvenTrackExceptionHandler();
    app.UseHsts();
}

// Rest of pipeline...
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();

app.Run();

Example Response

Production Error Response JSON
{
  "type": "https://httpstatuses.com/404",
  "title": "Not Found",
  "status": 404,
  "detail": "An error occurred while processing your request.",
  "instance": "/api/products/999",
  "traceId": "00-abc123def456...",
  "correlationId": "xyz789"
}
Development Error Response JSON
{
  "type": "https://httpstatuses.com/404",
  "title": "Not Found",
  "status": 404,
  "detail": "Product with ID 999 not found",
  "instance": "/api/products/999",
  "traceId": "00-abc123def456...",
  "correlationId": "xyz789",
  "stackTrace": "   at InvenTrack.Services.ProductService..."
}

Best Practices

  • Exception handler first: Always register exception handling middleware first
  • Never expose stack traces in production: Security risk
  • Use Problem Details: Standard format for API errors
  • Log all exceptions: Include context (user, path, trace ID)
  • Map exception types to status codes: ArgumentException → 400, etc.
  • Include trace/correlation IDs: Help debugging
  • Different responses for dev/prod: Details in dev, generic in prod
  • Use structured logging: Makes searching logs easier
  • Don't catch everything: Let critical errors bubble up
  • Test error handling: Verify responses and logging

Key Takeaways

  • Developer Exception Page: Detailed errors in development
  • UseExceptionHandler: Production error handling
  • Custom middleware: Full control over error responses
  • Problem Details (RFC 7807): Standard error format
  • Map exception types to HTTP status codes
  • Log exceptions with context (user, path, trace ID)
  • Never expose sensitive information in production
  • Include trace/correlation IDs for debugging
  • Different error details for dev vs production
  • Exception handler must be first in the pipeline
🎉 Part V Complete!

Congratulations! You've completed Part V: Middleware & Pipeline. You now understand:

  • ✅ The request pipeline and how it works
  • ✅ Built-in middleware (static files, CORS, auth, etc.)
  • ✅ Creating custom middleware (inline, convention-based, factory-based)
  • ✅ Middleware ordering and why it matters
  • ✅ Exception handling middleware and best practices

You're building a solid foundation in ASP.NET Core! The middleware pipeline is the backbone of every ASP.NET Core application. Keep up the great work! 🚀