Skip to main content

.NET Exception Hierarchy

Introduction

When developing applications in .NET, you'll inevitably encounter errors. Understanding how .NET organizes these errors through its exception hierarchy is crucial for effective error handling. This knowledge helps you catch specific exceptions, create custom exception types, and write more robust code.

The .NET exception system is built around a hierarchical class structure, with the base System.Exception class at the top. This hierarchy allows for categorization of different error types and enables you to handle exceptions with varying levels of specificity.

The Exception Class Hierarchy

In .NET, all exceptions derive from the System.Exception class. Let's examine the structure:

System.Object
└── System.Exception
├── System.SystemException
│ ├── System.ArgumentException
│ │ ├── System.ArgumentNullException
│ │ └── System.ArgumentOutOfRangeException
│ ├── System.NullReferenceException
│ ├── System.IndexOutOfRangeException
│ ├── System.InvalidOperationException
│ ├── System.IO.IOException
│ ├── System.DivideByZeroException
│ └── Many others...
└── System.ApplicationException
└── Your custom exceptions

System.Exception (Base Class)

System.Exception is the base class for all exceptions in .NET. It provides important properties and methods that all derived exception classes inherit:

csharp
try
{
// Code that may throw an exception
}
catch (Exception ex)
{
// Accessing common Exception properties
Console.WriteLine($"Message: {ex.Message}");
Console.WriteLine($"Source: {ex.Source}");
Console.WriteLine($"Stack Trace: {ex.StackTrace}");
Console.WriteLine($"Help Link: {ex.HelpLink}");
Console.WriteLine($"Inner Exception: {ex.InnerException?.Message}");
}

Key Properties:

  • Message: A human-readable description of the error
  • StackTrace: Records where the error occurred
  • InnerException: Contains the exception that caused the current exception
  • Source: The name of the application or object that caused the error
  • HelpLink: URL to help documentation

System.SystemException

SystemException serves as a base class for exceptions thrown by the runtime or that are system-related. These exceptions are usually thrown by the .NET Framework itself rather than application code.

Examples include:

  • NullReferenceException
  • IndexOutOfRangeException
  • InvalidOperationException
  • DivideByZeroException

System.ApplicationException

ApplicationException was originally intended as the base class for custom exceptions. However, Microsoft now recommends deriving custom exceptions directly from Exception instead.

Common Exception Types in .NET

Let's explore some of the most frequently encountered exception types:

ArgumentException & Its Derivatives

Thrown when a method is called with an invalid argument:

csharp
public void ProcessValue(int value)
{
if (value < 0)
{
throw new ArgumentException("Value cannot be negative.", nameof(value));
}

// Process the value
Console.WriteLine($"Processing value: {value}");
}

// Usage example with try-catch
try
{
ProcessValue(-5);
}
catch (ArgumentException ex)
{
Console.WriteLine($"Error: {ex.Message}");
// Output: Error: Value cannot be negative.
}

ArgumentNullException

A specialized version for null arguments:

csharp
public void ProcessName(string name)
{
if (name == null)
{
throw new ArgumentNullException(nameof(name), "Name cannot be null.");
}

Console.WriteLine($"Hello, {name}!");
}

ArgumentOutOfRangeException

For arguments that are outside of the allowed range:

csharp
public void SetAge(int age)
{
if (age < 0 || age > 120)
{
throw new ArgumentOutOfRangeException(nameof(age), "Age must be between 0 and 120.");
}

Console.WriteLine($"Age set to {age}");
}

NullReferenceException

Occurs when you attempt to access members of a null object reference:

csharp
string name = null;

try
{
int length = name.Length; // This will throw NullReferenceException
Console.WriteLine(length);
}
catch (NullReferenceException ex)
{
Console.WriteLine($"Error: {ex.Message}");
// Output: Error: Object reference not set to an instance of an object.
}

InvalidOperationException

Thrown when a method call is invalid for the object's current state:

csharp
public class FileProcessor
{
private bool _isInitialized = false;

public void Initialize()
{
// Initialization logic
_isInitialized = true;
Console.WriteLine("FileProcessor initialized successfully.");
}

public void ProcessFile(string filePath)
{
if (!_isInitialized)
{
throw new InvalidOperationException("FileProcessor must be initialized before processing files.");
}

Console.WriteLine($"Processing file: {filePath}");
}
}

// Usage example
var processor = new FileProcessor();
try
{
processor.ProcessFile("document.txt");
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"Error: {ex.Message}");
// Output: Error: FileProcessor must be initialized before processing files.
}

Creating Custom Exceptions

For application-specific errors, you may need to create custom exception classes:

csharp
// Custom exception class
public class UserNotFoundException : Exception
{
public string Username { get; }

public UserNotFoundException(string username)
: base($"User '{username}' was not found in the system.")
{
Username = username;
}

public UserNotFoundException(string username, Exception innerException)
: base($"User '{username}' was not found in the system.", innerException)
{
Username = username;
}
}

// Using the custom exception
public class UserService
{
private readonly List<string> _users = new List<string> { "Alice", "Bob", "Charlie" };

public void ProcessUser(string username)
{
if (!_users.Contains(username))
{
throw new UserNotFoundException(username);
}

Console.WriteLine($"Processing user: {username}");
}
}

// Usage example
var service = new UserService();
try
{
service.ProcessUser("David");
}
catch (UserNotFoundException ex)
{
Console.WriteLine($"Error: {ex.Message}");
Console.WriteLine($"Username that caused the error: {ex.Username}");
// Output:
// Error: User 'David' was not found in the system.
// Username that caused the error: David
}

Best practices for custom exceptions:

  1. Name them with the "Exception" suffix
  2. Derive directly from Exception (not ApplicationException)
  3. Make them serializable for distributed applications
  4. Include constructors with message and inner exception parameters
  5. Add custom properties that provide additional error context

Exception Handling Based on Hierarchy

The exception hierarchy enables you to catch exceptions at different levels of specificity:

csharp
try
{
// Code that could throw various exceptions
int[] numbers = new int[5];
numbers[10] = 5; // This will throw IndexOutOfRangeException
}
catch (ArgumentException ex)
{
// Catches ArgumentException and all its derived types
Console.WriteLine($"Argument error: {ex.Message}");
}
catch (IndexOutOfRangeException ex)
{
// Catches only IndexOutOfRangeException
Console.WriteLine($"Index error: {ex.Message}");
// Output: Index error: Index was outside the bounds of the array.
}
catch (SystemException ex)
{
// Catches any SystemException that wasn't caught above
Console.WriteLine($"System error: {ex.Message}");
}
catch (Exception ex)
{
// Catches any other exception
Console.WriteLine($"Unexpected error: {ex.Message}");
}

The when Clause (C# 6.0+)

You can further refine exception catching with the when clause:

csharp
try
{
string data = File.ReadAllText("data.txt");
ProcessData(data);
}
catch (IOException ex) when (ex.HResult == -2147024816) // File not found
{
Console.WriteLine("The data file was not found. Creating a new one...");
File.Create("data.txt");
}
catch (IOException ex) when (ex.HResult == -2147024864) // Path not found
{
Console.WriteLine("The specified path was not found.");
}
catch (IOException ex)
{
Console.WriteLine($"General IO error: {ex.Message}");
}

Real-World Example: Financial Transaction System

Let's look at a practical example of using the exception hierarchy in a financial system:

csharp
public class InsufficientFundsException : Exception
{
public decimal RequestedAmount { get; }
public decimal AvailableBalance { get; }

public InsufficientFundsException(decimal requested, decimal available)
: base($"Insufficient funds. Requested: ${requested}, Available: ${available}")
{
RequestedAmount = requested;
AvailableBalance = available;
}
}

public class AccountNotFoundException : Exception
{
public string AccountNumber { get; }

public AccountNotFoundException(string accountNumber)
: base($"Account {accountNumber} was not found.")
{
AccountNumber = accountNumber;
}
}

public class BankAccount
{
public string AccountNumber { get; }
public decimal Balance { get; private set; }

public BankAccount(string accountNumber, decimal initialBalance)
{
AccountNumber = accountNumber;
Balance = initialBalance;
}

public void Withdraw(decimal amount)
{
if (amount <= 0)
{
throw new ArgumentException("Withdrawal amount must be positive.", nameof(amount));
}

if (amount > Balance)
{
throw new InsufficientFundsException(amount, Balance);
}

Balance -= amount;
Console.WriteLine($"Withdrew ${amount}. New balance: ${Balance}");
}
}

public class BankService
{
private readonly Dictionary<string, BankAccount> _accounts = new Dictionary<string, BankAccount>();

public BankService()
{
// Initialize with some accounts
_accounts.Add("123456", new BankAccount("123456", 1000));
_accounts.Add("789012", new BankAccount("789012", 500));
}

public void ProcessWithdrawal(string accountNumber, decimal amount)
{
try
{
if (!_accounts.TryGetValue(accountNumber, out BankAccount account))
{
throw new AccountNotFoundException(accountNumber);
}

account.Withdraw(amount);
}
catch (ArgumentException ex)
{
Console.WriteLine($"Error: {ex.Message}");
// Log the error
}
catch (InsufficientFundsException ex)
{
Console.WriteLine($"Error: {ex.Message}");
Console.WriteLine($"Consider applying for an overdraft protection service.");
// Log the error
}
catch (AccountNotFoundException ex)
{
Console.WriteLine($"Error: {ex.Message}");
Console.WriteLine("Please check the account number and try again.");
// Log the error
}
catch (Exception ex)
{
Console.WriteLine($"Unexpected error: {ex.Message}");
// Log the error
throw; // Rethrow unexpected exceptions for higher-level handling
}
}
}

// Usage example
var bankService = new BankService();

// Case 1: Valid withdrawal
bankService.ProcessWithdrawal("123456", 500);
// Output: Withdrew $500. New balance: $500

// Case 2: Insufficient funds
bankService.ProcessWithdrawal("789012", 1000);
// Output:
// Error: Insufficient funds. Requested: $1000, Available: $500
// Consider applying for an overdraft protection service.

// Case 3: Invalid account
bankService.ProcessWithdrawal("000000", 100);
// Output:
// Error: Account 000000 was not found.
// Please check the account number and try again.

// Case 4: Invalid amount
bankService.ProcessWithdrawal("123456", -50);
// Output: Error: Withdrawal amount must be positive.

Summary

The .NET exception hierarchy provides a structured approach to handling errors in your applications:

  1. System.Exception is the base class for all exceptions
  2. Common derived exceptions include ArgumentException, NullReferenceException, and InvalidOperationException
  3. You can create custom exception types by inheriting from Exception
  4. The hierarchy allows catching exceptions at different levels of specificity
  5. Proper exception handling improves application robustness and user experience

Understanding this hierarchy helps you:

  • Write more precise exception handling code
  • Create meaningful custom exceptions
  • Follow .NET framework design guidelines
  • Create more maintainable and robust applications

Exercises

  1. Create a custom exception called ProductNotFoundException that includes properties for the product ID and product category.
  2. Write a method that demonstrates catching exceptions in order from most specific to most general.
  3. Extend the bank account example to include a TransferFunds method that handles multiple exception types.
  4. Create a hierarchy of custom exceptions for a library management system (e.g., BookNotFoundException, BorrowerNotFoundException).

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)