Skip to main content

.NET Exception Types

Introduction

Exception handling is a critical aspect of building robust applications in .NET. When something goes wrong during program execution, .NET uses exceptions to signal these error conditions. Understanding the different types of exceptions that can occur in a .NET application is essential for effective error handling and debugging.

In this tutorial, we'll explore the hierarchy of exception types in .NET, learn about common exceptions you'll encounter, and see how to work with each type through practical examples.

The .NET Exception Hierarchy

In the .NET Framework, all exceptions derive from the System.Exception base class. This creates a hierarchical structure that allows for specific error handling based on the exception type.

Here's a simplified view of the exception hierarchy:

System.Object
└── System.Exception
├── System.SystemException
│ ├── System.ArgumentException
│ ├── System.ArithmeticException
│ ├── System.NullReferenceException
│ ├── System.InvalidOperationException
│ └── ...
└── System.ApplicationException
└── (Custom exceptions)

Let's examine some of the most important exception types you'll encounter.

Common Exception Types

System.Exception

This is the base class for all exceptions in .NET. It provides properties like Message, StackTrace, and InnerException that are available to all derived exception types.

csharp
try
{
throw new Exception("This is a generic exception");
}
catch (Exception ex)
{
Console.WriteLine($"Exception Message: {ex.Message}");
Console.WriteLine($"Stack Trace: {ex.StackTrace}");
}

Output:

Exception Message: This is a generic exception
Stack Trace: at Program.<Main>$(String[] args) in C:\Projects\ConsoleApp1\Program.cs:line 7

These exceptions occur when a method is called with invalid arguments.

ArgumentException

Thrown when one of the arguments provided to a method is invalid.

csharp
public void ProcessName(string name)
{
if (name.Contains(" "))
{
throw new ArgumentException("Name cannot contain spaces", nameof(name));
}

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

// Usage example
try
{
ProcessName("John Doe");
}
catch (ArgumentException ex)
{
Console.WriteLine($"Error: {ex.Message}");
Console.WriteLine($"Parameter name: {ex.ParamName}");
}

Output:

Error: Name cannot contain spaces
Parameter name: name

ArgumentNullException

A more specific version of ArgumentException that's thrown when a null reference is passed to a method that doesn't accept it.

csharp
public void ProcessOrder(Order order)
{
if (order == null)
{
throw new ArgumentNullException(nameof(order), "Order cannot be null");
}

// Process the order...
}

// Usage example
try
{
ProcessOrder(null);
}
catch (ArgumentNullException ex)
{
Console.WriteLine($"Error: {ex.Message}");
}

Output:

Error: Order cannot be null (Parameter 'order')

ArgumentOutOfRangeException

Thrown when the value of an argument is outside the allowable range.

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

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

// Usage example
try
{
SetAge(150);
}
catch (ArgumentOutOfRangeException ex)
{
Console.WriteLine($"Error: {ex.Message}");
Console.WriteLine($"Actual value was: {ex.ActualValue}");
}

Output:

Error: Age must be between 0 and 120 (Parameter 'age')
Actual value was: 150

System.NullReferenceException

This exception occurs when you try to access a member on a reference type variable that points to null.

csharp
try
{
string name = null;
int length = name.Length; // This will throw NullReferenceException
}
catch (NullReferenceException ex)
{
Console.WriteLine($"Error: {ex.Message}");
}

Output:

Error: Object reference not set to an instance of an object.

System.InvalidOperationException

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

csharp
public class BankAccount
{
public decimal Balance { get; private set; }
public bool IsLocked { get; private set; }

public void Deposit(decimal amount)
{
if (IsLocked)
{
throw new InvalidOperationException("Cannot deposit to a locked account");
}

Balance += amount;
}

public void Lock()
{
IsLocked = true;
}
}

// Usage example
try
{
var account = new BankAccount();
account.Lock();
account.Deposit(100);
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"Operation failed: {ex.Message}");
}

Output:

Operation failed: Cannot deposit to a locked account

System.IndexOutOfRangeException

Thrown when attempting to access an element of an array or collection with an index that is outside its bounds.

csharp
try
{
int[] numbers = { 1, 2, 3, 4, 5 };
int value = numbers[10]; // This will throw IndexOutOfRangeException
}
catch (IndexOutOfRangeException ex)
{
Console.WriteLine($"Error: {ex.Message}");
}

Output:

Error: Index was outside the bounds of the array.

System.IO.IOException

This is the base class for exceptions that are thrown when an I/O error occurs.

csharp
try
{
// Attempt to read from a file that doesn't exist
string content = File.ReadAllText("nonexistent-file.txt");
}
catch (FileNotFoundException ex)
{
Console.WriteLine($"File error: {ex.Message}");
Console.WriteLine($"File path: {ex.FileName}");
}
catch (IOException ex)
{
Console.WriteLine($"I/O error: {ex.Message}");
}

Output:

File error: Could not find file 'C:\Projects\ConsoleApp1\bin\Debug\net6.0\nonexistent-file.txt'.
File path: C:\Projects\ConsoleApp1\bin\Debug\net6.0\nonexistent-file.txt

System.FormatException

Thrown when the format of an argument does not meet the parameter specifications of the invoked method.

csharp
try
{
string input = "not-a-number";
int number = int.Parse(input); // This will throw FormatException
}
catch (FormatException ex)
{
Console.WriteLine($"Format error: {ex.Message}");
}

Output:

Format error: The input string 'not-a-number' was not in a correct format.

Creating Custom Exception Types

In addition to the built-in exceptions, you can create your own custom exception types when the standard exceptions don't adequately describe your specific error conditions.

Here's how to create a custom exception:

csharp
public class InsufficientFundsException : Exception
{
public decimal CurrentBalance { get; }
public decimal WithdrawalAmount { get; }

public InsufficientFundsException(string message, decimal currentBalance, decimal withdrawalAmount)
: base(message)
{
CurrentBalance = currentBalance;
WithdrawalAmount = withdrawalAmount;
}

public InsufficientFundsException(string message, decimal currentBalance, decimal withdrawalAmount, Exception innerException)
: base(message, innerException)
{
CurrentBalance = currentBalance;
WithdrawalAmount = withdrawalAmount;
}
}

// Usage example
public class BankAccount
{
public decimal Balance { get; private set; }

public BankAccount(decimal initialBalance)
{
Balance = initialBalance;
}

public void Withdraw(decimal amount)
{
if (amount > Balance)
{
throw new InsufficientFundsException(
"Insufficient funds for this withdrawal",
Balance,
amount);
}

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

// Client code
try
{
var account = new BankAccount(100);
account.Withdraw(200); // This will throw our custom exception
}
catch (InsufficientFundsException ex)
{
Console.WriteLine($"Error: {ex.Message}");
Console.WriteLine($"Current balance: {ex.CurrentBalance:C}");
Console.WriteLine($"Attempted withdrawal: {ex.WithdrawalAmount:C}");
}

Output:

Error: Insufficient funds for this withdrawal
Current balance: $100.00
Attempted withdrawal: $200.00

Best Practices for Exception Types

  1. Choose the right exception type: Use the most specific exception type that accurately describes the error condition.

  2. Preserve the original exception: When catching and re-throwing exceptions, use the inner exception parameter to preserve the original stack trace.

csharp
try
{
// Code that might throw an exception
int.Parse("not-a-number");
}
catch (FormatException ex)
{
// Wrap the original exception with additional context
throw new ApplicationException("Error processing user input", ex);
}
  1. Don't use exceptions for normal flow control: Exceptions should be used for exceptional circumstances, not as a way to control normal program flow.

  2. Document the exceptions your methods can throw: Use XML documentation to clearly indicate which exceptions your methods can throw.

csharp
/// <summary>
/// Withdraws money from the account.
/// </summary>
/// <param name="amount">The amount to withdraw.</param>
/// <exception cref="ArgumentException">Thrown when amount is negative.</exception>
/// <exception cref="InsufficientFundsException">Thrown when there are insufficient funds.</exception>
public void Withdraw(decimal amount)
{
// Implementation...
}

Real-World Example: A File Processing Utility

Let's put our knowledge of exception types together in a simple file processing utility:

csharp
public class FileProcessor
{
public void ProcessFile(string filePath, string outputPath)
{
// Input validation
if (string.IsNullOrEmpty(filePath))
throw new ArgumentNullException(nameof(filePath));

if (string.IsNullOrEmpty(outputPath))
throw new ArgumentNullException(nameof(outputPath));

if (!File.Exists(filePath))
throw new FileNotFoundException("The specified input file was not found.", filePath);

try
{
// Read the file content
string content = File.ReadAllText(filePath);

// Process the content (e.g., convert to uppercase)
string processedContent = content.ToUpper();

// Write the processed content to the output file
File.WriteAllText(outputPath, processedContent);

Console.WriteLine($"File processed successfully. Output: {outputPath}");
}
catch (IOException ex)
{
throw new ApplicationException("An error occurred during file I/O operations.", ex);
}
catch (UnauthorizedAccessException ex)
{
throw new ApplicationException("Access to the file was denied.", ex);
}
}
}

// Usage
try
{
var processor = new FileProcessor();
processor.ProcessFile("input.txt", "output.txt");
}
catch (ArgumentNullException ex)
{
Console.WriteLine($"Invalid argument: {ex.ParamName} cannot be null.");
}
catch (FileNotFoundException ex)
{
Console.WriteLine($"File not found: {ex.FileName}");
}
catch (ApplicationException ex)
{
Console.WriteLine($"Application error: {ex.Message}");
if (ex.InnerException != null)
{
Console.WriteLine($"Caused by: {ex.InnerException.Message}");
}
}
catch (Exception ex)
{
Console.WriteLine($"Unexpected error: {ex.Message}");
}

This example demonstrates how different exception types are used to handle various error conditions that might occur during file processing.

Summary

Understanding .NET exception types is crucial for writing robust applications. In this guide, we've covered:

  • The exception hierarchy in .NET
  • Common built-in exception types and when to use them
  • How to create custom exception types
  • Best practices for working with exceptions
  • A real-world example of exception handling in a file processing utility

By using the right exception types and handling them appropriately, you can create more robust and maintainable applications that gracefully handle error conditions.

Additional Resources

Exercise

  1. Create a Calculator class that performs basic arithmetic operations (add, subtract, multiply, divide). Implement proper exception handling for each operation.

  2. Create a custom exception class called NegativeResultException that is thrown when an operation would result in a negative number.

  3. Write a program that reads user input as numbers and performs calculations. Handle all possible exceptions that could occur (like invalid input, division by zero, etc.).



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