Exception Handling, Custom Errors & Filters Masterclass

โšก C# 12 & .NET 8 ๐ŸŸข Chapter 25 of 35 ๐Ÿ“‚ Phase 9: Exceptions, Files & JSON ๐Ÿ“… 2026 Edition
๐Ÿ“Œ Covered in this chapter: Error vs Exception ยท try/catch/finally ยท throw ยท Custom Exceptions ยท Exception Filters (when) ยท Multiple Catch Blocks ยท Exception Logging ยท Best Practices

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.

1Error vs Exception โ€” What is an Exception?

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 ExceptionWhen It Occurs
DivideByZeroExceptionInteger division by zero (e.g., 10 / 0).
NullReferenceExceptionAccessing a member on a null object reference.
IndexOutOfRangeExceptionArray index outside valid bounds.
InvalidCastExceptionInvalid explicit cast between incompatible types.
FormatExceptionint.Parse() fails on invalid string format.
FileNotFoundExceptionAccessing a file that does not exist on disk.
ArgumentNullExceptionNull argument passed to a method that disallows it.
ArgumentOutOfRangeExceptionArgument value outside acceptable range.
OverflowExceptionArithmetic operation exceeds type's value range (in checked context).
2try, catch, finally & throw
C# โ€” try, catch, finally, throw โ–ถ Run in Compiler
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).");
}
๐Ÿ” Execution Flow Breakdown:
  • 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.).
3Custom Exceptions & Exception Filters
C# โ€” Custom Exception Class โ–ถ Run in Compiler
// 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.");
}
4Exception Best Practices

โœ… 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.

5Technical FAQs

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.