Exception Handling  

⚙️ Mastering Exception Handling in .NET Core — Complete Guide with Examples

🎯 Introduction

In real-world .NET Core applications — from Web APIs to microservicesexceptions are inevitable.
What matters most is how you handle them gracefully so your app never crashes unexpectedly and users get meaningful error responses.

This article covers all ways to handle exceptions in .NET Core, including:

  • Try–Catch–Finally

  • Global Exception Handling

  • Middleware-based handling

  • Exception Filters

  • Logging & Custom Exceptions

  • Best Practices with Real-World Scenarios

🧠 1. What is an Exception in .NET Core?

An exception is an unexpected runtime error that interrupts the normal execution flow.

For example:

int x = int.Parse("abc"); // FormatException

Here, trying to convert a non-numeric string to an integer causes a FormatException.

⚙️ 2. Basic Exception Handling — try, catch, finally

This is the most common and simplest way.

🧩 Example:

try
{
    int[] numbers = { 1, 2, 3 };
    Console.WriteLine(numbers[5]); // IndexOutOfRangeException
}
catch (IndexOutOfRangeException ex)
{
    Console.WriteLine($"Error: {ex.Message}");
}
finally
{
    Console.WriteLine("Execution completed.");
}

🔍 Explanation

  • try: Code that might throw an exception.

  • catch: Handles the exception.

  • finally: Always executes, even if an exception occurs (commonly used for cleanup, like closing DB connections).

🧩 3. Multiple Catch Blocks

You can handle different exceptions differently.

try
{
    string value = null;
    Console.WriteLine(value.Length); // NullReferenceException
}
catch (NullReferenceException ex)
{
    Console.WriteLine("Null reference error!");
}
catch (Exception ex)
{
    Console.WriteLine("General exception: " + ex.Message);
}

✅ Always keep the most specific exceptions first, and the generic Exception last.

🧱 4. Nested Try–Catch Blocks

Sometimes, you might handle exceptions differently in inner blocks.

try
{
    try
    {
        File.ReadAllText("data.txt");
    }
    catch (FileNotFoundException)
    {
        Console.WriteLine("File missing. Creating a new one...");
        File.WriteAllText("data.txt", "Sample content");
    }
}
catch (Exception ex)
{
    Console.WriteLine("Unexpected error: " + ex.Message);
}

💥 5. Throwing Exceptions Manually (throw)

You can explicitly raise exceptions using throw.

void ValidateAge(int age)
{
    if (age < 18)
        throw new ArgumentException("Age must be 18 or above.");
}

try
{
    ValidateAge(15);
}
catch (ArgumentException ex)
{
    Console.WriteLine(ex.Message);
}

👉 Useful for business validation logic.

🧰 6. Custom Exceptions

You can create your own exception classes for domain-specific errors.

🧩 Example:

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

public class BankAccount
{
    public decimal Balance { get; private set; } = 1000;

    public void Withdraw(decimal amount)
    {
        if (amount > Balance)
            throw new InsufficientBalanceException("Not enough funds.");
        
        Balance -= amount;
    }
}

💡 Usage

try
{
    var account = new BankAccount();
    account.Withdraw(2000);
}
catch (InsufficientBalanceException ex)
{
    Console.WriteLine(ex.Message);
}

✅ Helps distinguish business exceptions from system exceptions.

🌐 7. Global Exception Handling in .NET Core Web API

In web apps, handling exceptions globally is crucial for clean and consistent error responses.

🧩 Example: Using UseExceptionHandler in Program.cs

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.UseExceptionHandler("/error");

app.Map("/error", (HttpContext context) =>
{
    var exception = context.Features.Get<IExceptionHandlerFeature>()?.Error;
    return Results.Problem(detail: exception?.Message, title: "Server Error");
});

app.MapGet("/divide", () => 10 / 0); // Will trigger global handler

app.Run();

🔍 Explanation

  • UseExceptionHandler catches all unhandled exceptions.

  • Redirects to /error endpoint.

  • You can log and return structured error details (ProblemDetails).

🧱 8. Custom Exception Middleware

For more control, you can create a custom middleware.

🧩 Middleware Example

public class ExceptionMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ExceptionMiddleware> _logger;

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

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "An unhandled exception occurred.");
            context.Response.StatusCode = 500;
            await context.Response.WriteAsJsonAsync(new
            {
                StatusCode = 500,
                Message = "Internal Server Error",
                Detail = ex.Message
            });
        }
    }
}

💡 Register Middleware

app.UseMiddleware<ExceptionMiddleware>();

✅ This approach is ideal for microservices and APIs — you can centralize all exception handling, logging, and custom responses.

⚖️ 9. Exception Filters (For MVC / Web API)

You can handle exceptions using filters.

🧩 Example:

public class CustomExceptionFilter : IExceptionFilter
{
    private readonly ILogger<CustomExceptionFilter> _logger;

    public CustomExceptionFilter(ILogger<CustomExceptionFilter> logger)
    {
        _logger = logger;
    }

    public void OnException(ExceptionContext context)
    {
        _logger.LogError(context.Exception, "Unhandled exception");
        context.Result = new ObjectResult(new
        {
            Error = context.Exception.Message,
            Status = 500
        })
        {
            StatusCode = 500
        };
    }
}

💡 Register Globally

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

✅ Exception filters are great for MVC and API controllers — they intercept exceptions and produce consistent JSON responses.

📋 10. Logging Exceptions

Use built-in logging with ILogger or integrate with providers like Serilog, NLog, or Application Insights.

🧩 Example:

try
{
    int result = 10 / 0;
}
catch (Exception ex)
{
    _logger.LogError(ex, "An error occurred while dividing numbers.");
}

✅ Always log exceptions with stack traces for debugging production issues.

🧩 11. Global Exception Patterns in ASP.NET Core

PatternDescriptionBest For
try-catchLocal handlingSmall operations
Global middlewareCentralizedWeb APIs / Microservices
Exception FiltersController-levelMVC apps
UseExceptionHandlerBuilt-in global handlerGeneral APIs
Custom ExceptionDomain-level clarityBusiness rules

💡 12. Best Practices for Exception Handling

✅ Do:

  • Catch specific exceptions, not just Exception.

  • Use custom exceptions for business logic.

  • Always log exceptions with context.

  • Use global handlers to avoid repetitive try-catch.

  • Return standardized error responses in APIs.

❌ Don’t:

  • Swallow exceptions silently.

  • Overuse try–catch (use global handlers instead).

  • Throw exceptions in normal flow (use validation instead).

🧠 Real-World Scenario: Exception Handling in a .NET Core API

🔹 Problem:

An e-commerce API has order and payment services. Sometimes payment gateway fails.

🔹 Solution:

Use a CustomExceptionMiddleware:

  • Catch PaymentGatewayException

  • Log it in Serilog

  • Return a friendly JSON:

{"statusCode": 400,"message": "Payment failed. Please try again."}

This way, users get meaningful messages, and developers get detailed logs.

📘 Cheat Sheet

TechniqueScopeUse Case
Try-Catch-FinallyLocalHandle single risky operation
Multiple CatchLocalDifferent exceptions
ThrowLocalRaise manual exceptions
Custom ExceptionDomainBusiness logic errors
UseExceptionHandlerGlobalWeb API handling
MiddlewareGlobalCentralized logging & response
Exception FiltersControllerMVC-based APIs

🏁 Conclusion

Exception handling in .NET Core is more than just using try-catch.
It’s about designing a robust, reliable, and user-friendly system that logs, monitors, and recovers gracefully.

By combining:

  • Try–catch for local errors

  • Middleware for global handling

  • Custom exceptions for business logic

  • Logging for monitoring

…you can build production-ready .NET Core applications that are stable, maintainable, and easy to debug.