C# Exception Hierarchy
Introduction
When developing applications in C#, dealing with errors and unexpected situations is inevitable. The C# language provides a robust mechanism for handling these situations through exceptions. Understanding the exception hierarchy is crucial for writing code that gracefully handles errors and maintains application stability.
In this guide, we'll explore the structure of C#'s exception hierarchy, understand the relationships between different exception types, and learn how to use this knowledge to implement effective exception handling strategies.
The Base: System.Exception
At the root of all C# exceptions is the System.Exception
class. Every exception in C# inherits either directly or indirectly from this base class, which provides the fundamental properties and methods that all exceptions share.
Key properties of the System.Exception
class include:
- Message: A human-readable description of the error
- StackTrace: A string representing the call stack at the time the exception was thrown
- InnerException: The exception that caused the current exception (if any)
- HResult: A numeric error code (primarily for interoperability)
Here's a basic example showing how to access these properties:
try
{
// Code that might cause an exception
int result = 10 / 0;
}
catch (Exception ex)
{
Console.WriteLine($"Exception Message: {ex.Message}");
Console.WriteLine($"Stack Trace: {ex.StackTrace}");
Console.WriteLine($"Inner Exception: {ex.InnerException?.Message ?? "None"}");
Console.WriteLine($"HResult: {ex.HResult}");
}
Output:
Exception Message: Attempted to divide by zero.
Stack Trace: at ConsoleApp.Program.Main() in C:\Projects\Program.cs:line 11
Inner Exception: None
HResult: -2147352558
First-Level Hierarchy
Directly beneath System.Exception
, the .NET Framework defines two main categories of exceptions:
1. System.SystemException
These are exceptions thrown by the Common Language Runtime (CLR) or that are predefined in the .NET Framework. Examples include:
NullReferenceException
IndexOutOfRangeException
DivideByZeroException
ArgumentException
InvalidCastException
2. System.ApplicationException
This was originally intended for exceptions defined by application programmers to differentiate between system exceptions and application exceptions. However, Microsoft now recommends deriving custom exceptions directly from System.Exception
instead of ApplicationException
.
Key Exception Classes in the Hierarchy
Let's explore some important exception types you'll frequently encounter:
ArgumentException Family
These exceptions are thrown when a method receives an invalid argument.
// ArgumentException: Base class for argument exceptions
public void ProcessName(string name)
{
if (string.IsNullOrEmpty(name))
{
throw new ArgumentException("Name cannot be null or empty", nameof(name));
}
// Process the name...
}
// ArgumentNullException: More specific than ArgumentException
public void RegisterUser(User user)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user), "User cannot be null");
}
// Register the user...
}
// ArgumentOutOfRangeException: For arguments outside expected range
public void SetAge(int age)
{
if (age < 0 || age > 120)
{
throw new ArgumentOutOfRangeException(nameof(age), "Age must be between 0 and 120");
}
// Set the age...
}
InvalidOperationException
Thrown when a method call is invalid in an object's current state:
public class FileProcessor
{
private bool _isInitialized = false;
public void Initialize()
{
// Initialization code...
_isInitialized = true;
}
public void ProcessFile(string filePath)
{
if (!_isInitialized)
{
throw new InvalidOperationException("FileProcessor must be initialized before processing files");
}
// Process the file...
}
}
NullReferenceException
This occurs when you try to access members of an object that is null:
// This code will throw a NullReferenceException
string name = null;
int length = name.Length; // NullReferenceException thrown here
It's generally better to use null checking to prevent these exceptions rather than catching them:
string name = null;
int length = name?.Length ?? 0; // Null conditional operator prevents exception
FormatException
Thrown when the format of an argument does not meet the parameter specifications:
try
{
string input = "not_a_number";
int number = int.Parse(input); // This will throw a FormatException
}
catch (FormatException ex)
{
Console.WriteLine($"Invalid format: {ex.Message}");
// A better approach would be to use TryParse:
if (int.TryParse(input, out int result))
{
Console.WriteLine($"Parsed number: {result}");
}
else
{
Console.WriteLine("Could not parse the input as a number");
}
}
IOException and Derived Classes
These exceptions are thrown when I/O operations fail:
try
{
// Attempt to read from a file that might not exist
string content = File.ReadAllText("nonexistent_file.txt");
}
catch (FileNotFoundException ex)
{
Console.WriteLine($"File not found: {ex.FileName}");
}
catch (IOException ex)
{
Console.WriteLine($"I/O error: {ex.Message}");
}
Visual Representation of the Hierarchy
Here's a simplified view of the C# exception hierarchy:
System.Object
└── System.Exception
├── System.SystemException
│ ├── System.ArithmeticException
│ │ └── System.DivideByZeroException
│ ├── System.ArrayTypeMismatchException
│ ├── System.IndexOutOfRangeException
│ ├── System.InvalidCastException
│ ├── System.NullReferenceException
│ ├── System.ArgumentException
│ │ ├── System.ArgumentNullException
│ │ ├── System.ArgumentOutOfRangeException
│ │ └── System.DuplicateWaitObjectException
│ ├── System.OutOfMemoryException
│ └── System.InvalidOperationException
│ └── System.ObjectDisposedException
└── System.ApplicationException (not recommended for use)
└── Custom Exceptions (derive directly from Exception)
Creating Custom Exceptions
When creating your own exceptions, it's best practice to:
- Derive directly from
Exception
or an appropriate existing exception class - End the class name with "Exception"
- Make the exception serializable
- Provide constructors that match the standard exception pattern
Here's an example of a well-designed custom exception:
using System;
using System.Runtime.Serialization;
[Serializable]
public class UserNotFoundException : Exception
{
public string Username { get; }
public UserNotFoundException() : base() { }
public UserNotFoundException(string message) : base(message) { }
public UserNotFoundException(string message, Exception innerException)
: base(message, innerException) { }
public UserNotFoundException(string message, string username)
: base(message)
{
Username = username;
}
// This constructor is needed for serialization
protected UserNotFoundException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
Username = info.GetString(nameof(Username));
}
// Override GetObjectData for serialization
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
base.GetObjectData(info, context);
info.AddValue(nameof(Username), Username);
}
}
Using the custom exception:
public User FindUser(string username)
{
User user = _repository.GetUserByName(username);
if (user == null)
{
throw new UserNotFoundException($"User '{username}' was not found in the database", username);
}
return user;
}
// Usage with proper exception handling
try
{
User user = FindUser("johndoe");
Console.WriteLine($"Found user: {user.Name}");
}
catch (UserNotFoundException ex)
{
Console.WriteLine($"Error: {ex.Message}");
Console.WriteLine($"Username that caused the error: {ex.Username}");
}
Real-World Application: Layered Exception Handling
In real-world applications, effective exception handling often involves multiple layers. Let's look at a practical example of a web application with a database:
// Data access layer
public class UserRepository
{
public User GetUserById(int id)
{
try
{
// Database access code here
using (var connection = new SqlConnection(_connectionString))
{
connection.Open();
// Execute query...
// If user not found, return null
}
}
catch (SqlException ex)
{
// Translate data access specific exception to a more generic one
throw new DatabaseAccessException("Error accessing user database", ex);
}
return null;
}
}
// Business logic layer
public class UserService
{
private readonly UserRepository _repository;
public UserService(UserRepository repository)
{
_repository = repository;
}
public User GetActiveUser(int id)
{
try
{
var user = _repository.GetUserById(id);
if (user == null)
{
throw new UserNotFoundException($"User with ID {id} not found", id.ToString());
}
if (!user.IsActive)
{
throw new InactiveUserException($"User with ID {id} is not active", user);
}
return user;
}
catch (DatabaseAccessException)
{
// Rethrow database exceptions
throw;
}
catch (Exception ex) when (!(ex is UserNotFoundException || ex is InactiveUserException))
{
// Log unexpected errors and convert to business exception
_logger.LogError(ex, "Unexpected error getting user {UserId}", id);
throw new UserOperationException($"Failed to retrieve user with ID {id}", ex);
}
}
}
// API/Presentation layer
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
private readonly UserService _userService;
public UsersController(UserService userService)
{
_userService = userService;
}
[HttpGet("{id}")]
public ActionResult<UserDto> GetUser(int id)
{
try
{
var user = _userService.GetActiveUser(id);
return Ok(MapToDto(user));
}
catch (UserNotFoundException ex)
{
return NotFound(new { message = ex.Message });
}
catch (InactiveUserException ex)
{
return BadRequest(new { message = ex.Message });
}
catch (DatabaseAccessException ex)
{
// Log database error
return StatusCode(503, new { message = "Service temporarily unavailable" });
}
catch (Exception ex)
{
// Log unexpected error
return StatusCode(500, new { message = "An unexpected error occurred" });
}
}
}
This example demonstrates how exceptions can be:
- Created at the appropriate layer
- Translated between layers to hide implementation details
- Handled differently based on their type
- Used to return appropriate HTTP status codes in an API context
Best Practices for Working with the Exception Hierarchy
- Catch Specific Exceptions First: Always arrange catch blocks from most specific to most general.
try
{
// Code that might throw exceptions
}
catch (ArgumentNullException ex)
{
// Handle null argument
}
catch (ArgumentException ex)
{
// Handle other argument problems
}
catch (Exception ex)
{
// Handle any other exception
}
-
Don't Catch Exceptions You Can't Handle: Only catch exceptions if you can meaningfully handle them.
-
Don't Swallow Exceptions: Never use empty catch blocks that hide errors.
// Bad practice
try
{
// Code
}
catch (Exception) { } // Don't do this!
// Better practice
try
{
// Code
}
catch (Exception ex)
{
// Log the error
_logger.LogError(ex, "An error occurred");
// Either handle it or rethrow
throw;
}
- Use Exception Filters: In C# 6 and later, use exception filters to conditionally catch exceptions.
try
{
// Code that might throw
}
catch (Exception ex) when (ex.Message.Contains("Timeout"))
{
// Handle timeout-related exceptions
}
catch (Exception ex) when (_logger.LogAndReturnFalse(ex))
{
// This block never executes, but the exception gets logged
// Because LogAndReturnFalse always returns false
}
- Consider Performance: Exception handling is expensive. Use it for exceptional cases, not for normal control flow.
Summary
Understanding the C# exception hierarchy is essential for writing robust and maintainable code. The hierarchy starts with the base System.Exception
class and branches out into system exceptions and application-specific exceptions.
Key points to remember:
- All exceptions derive from
System.Exception
- Catch specific exceptions before more general ones
- Create custom exceptions when needed to represent application-specific error conditions
- Use exception properties like
Message
,StackTrace
, andInnerException
for detailed error information - Implement proper exception handling strategies based on your application's architecture
- Don't use exceptions for normal control flow as they impact performance
By mastering the exception hierarchy, you'll be able to write code that gracefully handles errors, provides meaningful feedback to users, and maintains application stability even in the face of unexpected situations.
Additional Resources
- Microsoft Documentation on Exception Class
- Best Practices for Exceptions
- Exception Handling in C# - Microsoft Learn
Exercises
-
Create a custom exception class called
ProductNotFoundException
that includes properties for the product ID and category. -
Write a method that demonstrates the proper use of exception handling with multiple catch blocks, catching exceptions in order from most specific to most general.
-
Refactor the following code to use better exception handling practices:
csharppublic double Divide(string numerator, string denominator)
{
try
{
return int.Parse(numerator) / int.Parse(denominator);
}
catch (Exception ex)
{
Console.WriteLine("Error occurred.");
return 0;
}
} -
Create a class that implements the IDisposable pattern and throws an ObjectDisposedException when someone tries to use it after disposal.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)