.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:
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:
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:
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:
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:
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:
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:
// 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:
- Name them with the "Exception" suffix
- Derive directly from
Exception
(notApplicationException
) - Make them serializable for distributed applications
- Include constructors with message and inner exception parameters
- 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:
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:
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:
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:
- System.Exception is the base class for all exceptions
- Common derived exceptions include ArgumentException, NullReferenceException, and InvalidOperationException
- You can create custom exception types by inheriting from Exception
- The hierarchy allows catching exceptions at different levels of specificity
- 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
- Create a custom exception called
ProductNotFoundException
that includes properties for the product ID and product category. - Write a method that demonstrates catching exceptions in order from most specific to most general.
- Extend the bank account example to include a
TransferFunds
method that handles multiple exception types. - 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! :)