Exception Handling, Custom Errors & Filters Masterclass
Welcome to Phase 9 (Chapter 25): C# Exception Handling, Custom Exceptions & Filters Masterclass! Exception handling protects your application from unexpected runtime errors. In this chapter, we cover try, catch, finally, throw, creating custom exception classes, multiple catch blocks, exception filters (when), exception logging best practices, and avoiding common exception anti-patterns.
An Exception is an object that represents an unexpected or invalid situation occurring at runtime that interrupts normal program execution. In C#, all exceptions inherit from System.Exception. Unlike compile-time errors, exceptions happen during program execution and can be caught and handled gracefully using structured exception handling.
| Built-In Exception | When It Occurs |
|---|---|
DivideByZeroException | Integer division by zero (e.g., 10 / 0). |
NullReferenceException | Accessing a member on a null object reference. |
IndexOutOfRangeException | Array index outside valid bounds. |
InvalidCastException | Invalid explicit cast between incompatible types. |
FormatException | int.Parse() fails on invalid string format. |
FileNotFoundException | Accessing a file that does not exist on disk. |
ArgumentNullException | Null argument passed to a method that disallows it. |
ArgumentOutOfRangeException | Argument value outside acceptable range. |
OverflowException | Arithmetic operation exceeds type's value range (in checked context). |
try
{
int result = 10 / 0; // Throws DivideByZeroException!
Console.WriteLine(result);
}
catch (DivideByZeroException ex)
{
Console.WriteLine($"Cannot divide by zero. Details: {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"Unexpected error: {ex.Message}");
}
finally
{
// ALWAYS executes regardless of whether an exception was thrown or caught
Console.WriteLine("Finally block: Cleanup complete (always runs).");
}
- try block: Contains code that might throw an exception. If an exception occurs, execution immediately jumps to the matching catch block.
- catch block: Intercepts the exception. Multiple catch blocks can handle different exception types from most-specific to most-general.
- finally block: Executes unconditionally after try/catch, used for resource cleanup (closing files, database connections, etc.).
// Custom Exception โ inherit from Exception base class
public class InsufficientFundsException : Exception
{
public decimal RequiredAmount { get; }
public decimal AvailableBalance { get; }
public InsufficientFundsException(decimal required, decimal available)
: base($"Insufficient funds! Required: {required:C}, Available: {available:C}")
{
RequiredAmount = required;
AvailableBalance = available;
}
}
static void WithdrawFunds(decimal amount, decimal balance)
{
if (amount > balance)
throw new InsufficientFundsException(amount, balance);
Console.WriteLine($"Withdrawal of {amount:C} successful!");
}
try
{
WithdrawFunds(5000m, 1200m);
}
catch (InsufficientFundsException ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
// Exception Filter using 'when' clause
try
{
string? input = null;
Console.WriteLine(input!.ToUpper()); // NullReferenceException!
}
catch (NullReferenceException ex) when (ex.Message.Contains("Object"))
{
Console.WriteLine("Caught NullReferenceException matching 'Object' filter.");
}
โ Exception Handling Best Practices in Production C# Code:
โข Never swallow exceptions silently: Empty catch blocks catch (Exception) {} hide bugs and make debugging impossible.
โข Catch specific exceptions first: Order catch blocks from most-specific to most-generic. DivideByZeroException before Exception.
โข Use finally for cleanup: Always close file streams, database connections, and network sockets in finally blocks or using statements.
โข Use custom exceptions for domain errors: Create BusinessRuleException, ValidationException for meaningful error messages.
โข Log exceptions: Use structured logging (Serilog, Microsoft.Extensions.Logging) to record exception details.
Q1: What is the difference between throw and throw ex?
throw; re-throws the original exception preserving the original stack trace. throw ex; re-throws the exception but resets the stack trace to the current location, losing valuable debugging information. Always prefer bare throw;.
Q2: When should I use Exception Filters (when)?
Use exception filters when you want to catch an exception only under specific conditions without the overhead of catching and re-throwing it. Filters are evaluated before the catch block executes.