🎯 Introduction
In real-world .NET Core applications — from Web APIs to microservices — exceptions 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
| Pattern | Description | Best For |
|---|
try-catch | Local handling | Small operations |
| Global middleware | Centralized | Web APIs / Microservices |
| Exception Filters | Controller-level | MVC apps |
UseExceptionHandler | Built-in global handler | General APIs |
| Custom Exception | Domain-level clarity | Business 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:
{"statusCode": 400,"message": "Payment failed. Please try again."}
This way, users get meaningful messages, and developers get detailed logs.
📘 Cheat Sheet
| Technique | Scope | Use Case |
|---|
| Try-Catch-Finally | Local | Handle single risky operation |
| Multiple Catch | Local | Different exceptions |
| Throw | Local | Raise manual exceptions |
| Custom Exception | Domain | Business logic errors |
| UseExceptionHandler | Global | Web API handling |
| Middleware | Global | Centralized logging & response |
| Exception Filters | Controller | MVC-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.