Exception Handling  

Exception Handling in ASP.NET Core

Exception handling is one of the most important aspects of building stable, secure, and production-ready applications. Every application eventually encounters errors: invalid input, network failures, database connection issues, unhandled null values, or unauthorized access. The challenge is not avoiding these errors but handling them gracefully and consistently.

ASP.NET Core provides a rich and powerful exception-handling pipeline. This guide explains exception handling from the basics to advanced techniques, including real-world examples, global middleware, logging, structured API responses, custom exceptions, and best practices.

This article covers the following:

  • What exceptions are

  • Why exception handling matters

  • Types of exceptions

  • Try–catch–finally

  • ASP.NET Core built-in error handling

  • Global exception handling middleware

  • Exception filters

  • Logging exceptions

  • Custom business exceptions

  • Validation error handling

  • Returning consistent API error responses

  • Production best practices

1. What is an Exception?

An exception is an unexpected event that occurs during program execution and disrupts the normal flow of the application.

Common examples include:

  • Trying to divide by zero

  • Attempting to read null values

  • Accessing invalid array indexes

  • Database connection failures

  • Invalid type conversions

Exceptions help developers identify problems, but unhandled exceptions cause application crashes. This is why exception handling is essential.

2. Why Exception Handling Is Important

Proper exception handling provides several benefits:

  • Prevents application crashes

  • Displays user-friendly messages

  • Prevents sensitive information from leaking

  • Produces reliable API responses

  • Helps developers diagnose issues through logs

  • Improves maintainability and robustness

Without exception handling, end users may see application crashes or confusing error pages. With proper handling, errors become predictable, controlled, and secure.

3. Types of Exceptions in .NET

System Exceptions

These are built-in .NET exception types, such as:

  • NullReferenceException

  • ArgumentException

  • InvalidOperationException

  • FormatException

  • IndexOutOfRangeException

Application Exceptions

Custom exceptions created by developers to handle logical or business-related issues:

public class InvalidOrderException : Exception
{
    public InvalidOrderException(string message) : base(message) { }
}

Business Rule Exceptions

These represent domain-level issues, such as:

  • Insufficient balance

  • Invalid order ID

  • Inactive account

  • Unauthorized operation

4. Try–Catch–Finally - Basic Exception Handling

The simplest form of exception handling uses try, catch, and finally blocks.

try
{
    int x = 10;
    int y = 0;
    int result = x / y;
}
catch (DivideByZeroException ex)
{
    Console.WriteLine("Cannot divide by zero.");
}
catch (Exception ex)
{
    Console.WriteLine("An unexpected error occurred.");
}
finally
{
    Console.WriteLine("This block always executes.");
}

The finally block is optional and is typically used to release resources like database connections or file handles.

5. Exception Handling in ASP.NET Core

ASP.NET Core offers several layers of exception handling:

  1. Developer exception page

  2. Production exception handler using UseExceptionHandler

  3. Custom global exception handling middleware

  4. Exception filters

  5. Validation responses

  6. Structured error responses (Problem Details)

  7. Logging via ILogger or external log providers

Each technique has a specific use case depending on whether you are in development or production.

6. Developer Exception Page - Development Environment

This provides detailed error information, including:

  • Stack trace

  • Source file and line number

  • Error message

  • Request details

Enable it only in development:

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}

Never enable this in production because it exposes sensitive details.

7. Built-in Production Exception Handler - UseExceptionHandler

This middleware catches unhandled exceptions globally.

Step 1: Enable the handler in Program.cs

app.UseExceptionHandler("/error");

Step 2: Create the error endpoint

app.Map("/error", (HttpContext context) =>
{
    return Results.Problem("An unexpected error occurred.");
});

This handles all unhandled exceptions without exposing internal details.

8. Global Exception Handling Middleware - Preferred Method

Creating a custom middleware provides full control and centralizes all exception handling logic.

ExceptionMiddleware.cs

using System.Net;
using System.Text.Json;

public class ExceptionMiddleware : IMiddleware
{
    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        try
        {
            await next(context);
        }
        catch (Exception ex)
        {
            context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
            context.Response.ContentType = "application/json";

            var errorResponse = new
            {
                StatusCode = context.Response.StatusCode,
                Message = "An unexpected error occurred.",
                Detail = ex.Message
            };

            var json = JsonSerializer.Serialize(errorResponse);
            await context.Response.WriteAsync(json);
        }
    }
}

Register in the Program.cs

builder.Services.AddTransient<ExceptionMiddleware>();
app.UseMiddleware<ExceptionMiddleware>();

This approach is suitable for production APIs.

9. Exception Filters - MVC Specific

Exception filters allow centralized handling for controller actions.

CustomExceptionFilter.cs

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;

public class CustomExceptionFilter : IExceptionFilter
{
    public void OnException(ExceptionContext context)
    {
        context.Result = new JsonResult(new
        {
            Message = "An error occurred.",
            Detail = context.Exception.Message
        })
        { StatusCode = 500 };
    }
}

Register in the Program.cs

builder.Services.AddControllers(options =>
{
    options.Filters.Add<CustomExceptionFilter>();
});

Filters are useful when you want exception handling only for controllers, not for middleware-level requests.

10. Validation Exception Handling

ASP.NET Core automatically validates DTOs using data annotations.

Example DTO:

public class RegisterDto
{
    [Required]
    public string Email { get; set; }

    [MinLength(6)]
    public string Password { get; set; }
}

When validation fails, ASP.NET Core returns a structured 400 response:

{
  "errors": {
    "Password": ["The field Password must be a string with a minimum length of 6."]
  }
}

No additional exception handling is required for validation errors.

11. Structured Error Responses Using ProblemDetails

ASP.NET Core supports the Problem Details format (RFC 7807).

Example:

return Problem(
    title: "Internal Server Error",
    detail: "Database connection failed",
    statusCode: 500
);

This produces a structured response:

{
  "title": "Internal Server Error",
  "status": 500,
  "detail": "Database connection failed"
}

Consistent error responses improve debugging and client-side error handling.

12. Logging Exceptions Using ILogger

Logging is critical for diagnosing issues and monitoring applications.

try
{
    int.Parse("abc");
}
catch (Exception ex)
{
    _logger.LogError(ex, "Error while parsing number");
}

Logging providers supported in ASP.NET Core:

  • Console logging

  • Debug logging

  • Serilog

  • NLog

  • Seq

  • Application Insights

Logs should be centralized in production.

13. Custom Business Exceptions

Applications often need domain-level error handling.

public class InsufficientFundsException : Exception
{
    public InsufficientFundsException(string message) : base(message) { }
}

Usage:

if (balance < amount)
    throw new InsufficientFundsException("Insufficient funds to complete transaction.");

Catch these globally and return user-friendly messages.

14. Real-World Example: Handling Exceptions in a Controller

[HttpGet("{id}")]
public async Task<IActionResult> GetOrder(int id)
{
    var order = await _context.Orders.FindAsync(id);

    if (order == null)
        throw new KeyNotFoundException("Order not found");

    return Ok(order);
}

Global middleware converts this into a structured error response without exposing sensitive details.

15. Best Practices for Exception Handling in Production

  1. Never expose internal exception messages to users

  2. Always log exceptions with proper context

  3. Use global exception handling middleware

  4. Standardize error responses with ProblemDetails

  5. Use try-catch only where required

  6. Validate user input before processing

  7. Create custom exceptions for business logic

  8. Return proper HTTP status codes

  9. Separate domain, application, and infrastructure exceptions

Proper exception handling improves code quality, application reliability, and security.

Thank you for reading this complete guide on Exception Handling in ASP.NET Core. Exception handling is a critical skill for building stable, secure, and maintainable applications. By implementing global handlers, logging, structured responses, and custom exception logic, you ensure your APIs remain predictable and professional under all circumstances.