Skip to main content

2.7 - Error Handling

Error handling is essential for developing robust software applications. In C#, errors are managed through exceptionsβ€”objects that encapsulate error details that occur during program execution. Effective error handling helps the application to respond gracefully to unexpected issues, maintain security, and provide valuable feedback to users.

🧩 Visual Learning: What Happens When Errors Occur?​

Think of exceptions like fire alarms in a building:

Normal Program Flow:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Step 1 β”‚ ──► β”‚ Step 2 β”‚ ──► β”‚ Step 3 β”‚ ──► ...
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

When an Error Occurs:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Step 1 β”‚ ──► β”‚ Step 2 β”‚ β•³ β”‚ Step 3 β”‚ ──► ...
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ EXCEPTION! β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Error β”‚
β”‚ Handler β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

When an error occurs:

  1. Normal execution stops immediately
  2. The program looks for a handler that can deal with the error
  3. If a handler is found, it processes the error
  4. If no handler is found, the program crashes

πŸ’‘ Concept Breakdown: Why We Need Error Handling​

Imagine you're writing a program that reads a file:

  • Without error handling: If the file doesn't exist, your program crashes and the user sees a technical error message they don't understand.

  • With error handling: Your program can:

    • Detect that the file is missing
    • Show a friendly message to the user
    • Offer solutions (create the file, choose another file, etc.)
    • Continue running instead of crashing

Error handling makes your programs more robust and user-friendly.

2.7.1 - Understanding Exceptions​

Exceptions in C# provide a sophisticated way to handle runtime errors and other exceptional conditions without resorting to error-prone "error codes." This method enhances code robustness and maintainability by separating error-handling code from the main program logic.

πŸ”° Beginner's Corner: Understanding Exceptions​

In everyday language, an "exception" is something unusual or unexpected. In programming, it's the same idea:

  • An exception is C#'s way of saying "Something unexpected happened!"
  • When an error occurs, C# creates an exception object that contains information about what went wrong
  • The program then "throws" this exception, which interrupts normal execution
  • You can "catch" exceptions to handle errors gracefully

Think of it like playing catch with a hot potato:

  1. Something goes wrong (the potato gets hot)
  2. The code throws the hot potato (exception)
  3. Your error handler catches the hot potato and deals with it

2.7.1.1 - What Are Exceptions?​

An exception is an object that represents an error condition or an unexpected situation that occurs during the execution of a program. When an error occurs, the runtime creates an exception object containing information about the error, and then "throws" this object.

The exception object includes:

  • The type of exception (which indicates the nature of the error)
  • A message describing the error
  • The state of the call stack at the time the exception was thrown
  • Possibly additional context-specific information

Example: How Exceptions Occur

// This code will cause an exception
public void DemonstrateException()
{
// Attempting to divide by zero will cause a DivideByZeroException
int numerator = 10;
int denominator = 0;

// The following line will throw an exception at runtime
int result = numerator / denominator; // DivideByZeroException occurs here

// This line will never execute because of the exception
Console.WriteLine($"Result: {result}");
}

2.7.1.2 - Exception Hierarchy​

C# includes a hierarchy of exception classes deriving from the base class System.Exception. Understanding this hierarchy helps you catch and handle exceptions more effectively.

System.Object
└── System.Exception
β”œβ”€β”€ System.SystemException
β”‚ β”œβ”€β”€ System.ArithmeticException
β”‚ β”‚ └── System.DivideByZeroException
β”‚ β”œβ”€β”€ System.NullReferenceException
β”‚ β”œβ”€β”€ System.IndexOutOfRangeException
β”‚ β”œβ”€β”€ System.InvalidCastException
β”‚ └── ... (many others)
└── System.ApplicationException
└── ... (custom application exceptions)

2.7.1.3 - Common Exception Types​

Here are some of the most frequently encountered exception types in C# development:

Exception ClassDescriptionExample Scenario
System.ExceptionThe base class for all exceptions.Parent of all exceptions
System.IndexOutOfRangeExceptionThrown when attempting to access elements outside the bounds of an array or collectionint[] array = {1, 2, 3}; int item = array[5];
System.NullReferenceExceptionThrown when trying to use an object reference that has not been initializedstring name = null; int length = name.Length;
System.InvalidOperationExceptionThrown when an operation is attempted that is invalid for the object's current stateCalling a method on an object that's in an invalid state
System.ArgumentExceptionThrown when a method receives an argument that is not validPassing a negative number to a method requiring positive values
System.ArgumentNullExceptionThrown when a null argument is passed to a method that doesn't accept itProcessData(null) when null isn't allowed
System.FormatExceptionThrown when the format of an argument doesn't meet the parameter specificationsint.Parse("abc") when a number is expected
System.IO.IOExceptionThrown for errors encountered during input/output operationsAttempting to read from a file that is locked
System.DivideByZeroExceptionThrown when an attempt is made to divide an integer by zeroint result = 10 / 0;
System.OverflowExceptionThrown when an arithmetic operation results in an overflowbyte b = 255; b++;

Example: Common Exceptions in Action

/// <summary>
/// Demonstrates various common exceptions that can occur in C# code
/// </summary>
public void DemonstrateCommonExceptions()
{
// 1. IndexOutOfRangeException
try
{
int[] numbers = { 1, 2, 3 };
Console.WriteLine(numbers[5]); // Trying to access index 5 in a 3-element array
}
catch (IndexOutOfRangeException ex)
{
Console.WriteLine($"Array index error: {ex.Message}");
// Output: Array index error: Index was outside the bounds of the array.
}

// 2. NullReferenceException
try
{
string name = null;
Console.WriteLine(name.Length); // Trying to access a property of a null object
}
catch (NullReferenceException ex)
{
Console.WriteLine($"Null reference error: {ex.Message}");
// Output: Null reference error: Object reference not set to an instance of an object.
}

// 3. FormatException
try
{
int number = int.Parse("abc"); // Trying to parse a non-numeric string
}
catch (FormatException ex)
{
Console.WriteLine($"Format error: {ex.Message}");
// Output: Format error: Input string was not in a correct format.
}

// 4. DivideByZeroException
try
{
int result = 10 / 0; // Trying to divide by zero
}
catch (DivideByZeroException ex)
{
Console.WriteLine($"Division error: {ex.Message}");
// Output: Division error: Attempted to divide by zero.
}

// 5. ArgumentException
try
{
ProcessAge(-5); // Passing an invalid argument
}
catch (ArgumentException ex)
{
Console.WriteLine($"Argument error: {ex.Message}");
// Output: Argument error: Age cannot be negative. (Parameter 'age')
}
}

/// <summary>
/// Processes a person's age, throwing an exception if the age is invalid
/// </summary>
/// <param name="age">The age to process (must be non-negative)</param>
/// <exception cref="ArgumentException">Thrown when age is negative</exception>
public void ProcessAge(int age)
{
if (age < 0)
{
// Throwing an ArgumentException with a descriptive message
throw new ArgumentException("Age cannot be negative.", nameof(age));
}

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

For more detailed information on these and other exception types, please visit the Microsoft official documentation on exceptions.


2.7.1.4 - Basic Exception Handling​

Exception handling in C# is primarily done using try-catch blocks, which allow for graceful handling of errors in a controlled way. The basic structure consists of:

  1. A try block containing code that might throw exceptions
  2. One or more catch blocks that handle specific types of exceptions
  3. An optional finally block that executes regardless of whether an exception occurred

πŸ”° Beginner's Corner: Understanding Try-Catch​

Think of try-catch like a safety net for a trapeze artist:

  • The try block is where the trapeze artist performs dangerous stunts (risky code)
  • The catch blocks are safety nets positioned to catch the artist if they fall (handle errors)
  • The finally block is the exit procedure that happens whether the performance went well or not
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ try { β”‚
β”‚ Risky code that β”‚
β”‚ might cause errorsβ”‚
β”‚ } β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Did an error occur? β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
β”Œβ”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”
β”‚ β”‚
β–Ό β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”
β”‚ Yes β”‚ β”‚ No β”‚
β””β”€β”€β”€β”¬β”€β”€β”€β”˜ β””β”€β”€β”€β”¬β”€β”€β”€β”˜
β”‚ β”‚
β–Ό β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ catch { β”‚ β”‚
β”‚ Handle the β”‚ β”‚
β”‚ error β”‚ β”‚
β”‚ } β”‚ β”‚
β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚
β–Ό β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ finally { β”‚
β”‚ Code that always β”‚
β”‚ runs β”‚
β”‚ } β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ’‘ Concept Breakdown: Try-Catch in Action​

Let's see how try-catch works with a real-world example:

// Imagine a program that reads a user's age from input
try
{
Console.Write("Enter your age: ");
string input = Console.ReadLine();
int age = int.Parse(input); // This might throw an exception if input isn't a number

Console.WriteLine($"Next year, you'll be {age + 1} years old.");
}
catch (FormatException)
{
// This runs if the user entered something that's not a number
Console.WriteLine("That's not a valid number. Please enter a numeric age.");
}
catch (OverflowException)
{
// This runs if the number is too large for an int
Console.WriteLine("That number is too large. Please enter a reasonable age.");
}
finally
{
// This always runs, whether there was an error or not
Console.WriteLine("Thank you for using our program!");
}

The Try-Catch-Finally Structure​

try
{
// Code that might throw exceptions
}
catch (SpecificExceptionType ex)
{
// Handle this specific type of exception
}
catch (AnotherExceptionType ex)
{
// Handle another type of exception
}
catch (Exception ex)
{
// Handle any other exceptions not caught above
}
finally
{
// Code that always runs, whether an exception occurred or not
// Typically used for cleanup operations
}

⚠️ Common Pitfalls for Beginners​

  1. Catching exceptions that are too general

    try
    {
    // Some code
    }
    catch (Exception ex) // Too general! Catches ALL exceptions
    {
    // This will catch everything, even exceptions you might not know how to handle
    }

    Better approach:

    try
    {
    // Some code
    }
    catch (FileNotFoundException ex)
    {
    // Handle specifically missing files
    }
    catch (UnauthorizedAccessException ex)
    {
    // Handle specifically permission issues
    }
  2. Empty catch blocks

    try
    {
    // Some code
    }
    catch (Exception)
    {
    // Empty! This swallows the exception without handling it
    }

    This is dangerous because it hides errors without addressing them.

Exception Handling Flow​

When an exception occurs:

  1. The runtime looks for a matching catch block in the current method
  2. If no matching catch block is found, the exception "bubbles up" to the calling method
  3. This process continues up the call stack until a matching handler is found
  4. If no handler is found anywhere in the call stack, the program terminates with an unhandled exception

Example: Comprehensive File Handling with Exception Handling

/// <summary>
/// Demonstrates comprehensive exception handling with file operations
/// </summary>
/// <param name="filePath">Path to the file to process</param>
public void ProcessFile(string filePath)
{
// Declare resources outside the try block so they're accessible in finally
StreamReader reader = null;

try
{
// Attempt to read a file that might not exist
// This could throw FileNotFoundException, IOException, SecurityException, etc.
reader = new StreamReader(filePath);

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

// Read the file line by line
string line;
int lineNumber = 0;

while ((line = reader.ReadLine()) != null)
{
lineNumber++;
Console.WriteLine($"Line {lineNumber}: {line}");

// Try to parse each line as an integer
try
{
int value = int.Parse(line);
Console.WriteLine($" Parsed value: {value}");

// Demonstrate another potential exception
if (value == 0)
{
int result = 100 / value; // This will throw DivideByZeroException
}
}
catch (FormatException)
{
// Handle the case where the line isn't a valid integer
Console.WriteLine($" Warning: Line {lineNumber} is not a valid integer");

// Continue processing the file - don't let this stop us
continue;
}
catch (DivideByZeroException)
{
Console.WriteLine($" Warning: Cannot divide by zero on line {lineNumber}");
continue;
}
}
}
catch (FileNotFoundException ex)
{
// Handle the specific case where the file doesn't exist
Console.WriteLine($"Error: The file '{ex.FileName}' was not found.");
Console.WriteLine("Please check the file path and try again.");

// Log the error for debugging
Console.WriteLine($"Exception details: {ex.Message}");

// You might want to create the file or use a default one
CreateEmptyFile(filePath);
}
catch (IOException ex)
{
// Handle other I/O errors (file locked, disk full, etc.)
Console.WriteLine("Error: An I/O error occurred while reading the file.");
Console.WriteLine($"Exception details: {ex.Message}");

// You might want to retry after a delay
// RetryOperation(() => ProcessFile(filePath));
}
catch (UnauthorizedAccessException ex)
{
// Handle permission issues
Console.WriteLine("Error: You don't have permission to access this file.");
Console.WriteLine($"Exception details: {ex.Message}");

// You might want to request elevated permissions or use an alternative file
}
catch (Exception ex)
{
// Catch any other unexpected errors
// This is a "catch-all" handler that should be used carefully
Console.WriteLine("An unexpected error occurred:");
Console.WriteLine($"Exception type: {ex.GetType().Name}");
Console.WriteLine($"Message: {ex.Message}");
Console.WriteLine($"Stack trace: {ex.StackTrace}");

// Consider logging the error or notifying administrators
// LogError(ex);

// Depending on the application, you might want to:
// - Rethrow the exception: throw;
// - Throw a more specific exception: throw new ApplicationException("File processing failed", ex);
// - Return a failure status
}
finally
{
// Cleanup code that always runs, regardless of whether an exception occurred
if (reader != null)
{
// Close the file reader
reader.Close();
Console.WriteLine("File reader closed.");
}

Console.WriteLine("File processing operation completed.");
}
}

/// <summary>
/// Creates an empty file at the specified path
/// </summary>
/// <param name="filePath">Path where the file should be created</param>
private void CreateEmptyFile(string filePath)
{
try
{
// Create a new empty file
using (File.Create(filePath))
{
Console.WriteLine($"Created new empty file: {filePath}");
}
}
catch (Exception ex)
{
Console.WriteLine($"Failed to create empty file: {ex.Message}");
}
}

Using Statement for Resource Management​

The using statement provides a cleaner way to ensure that disposable resources (like file handles, database connections, etc.) are properly cleaned up, even if an exception occurs:

/// <summary>
/// Demonstrates the using statement for automatic resource cleanup
/// </summary>
/// <param name="filePath">Path to the file to process</param>
public void ProcessFileWithUsing(string filePath)
{
try
{
// The using statement ensures that the StreamReader is properly disposed
// even if an exception occurs within the block
using (StreamReader reader = new StreamReader(filePath))
{
string content = reader.ReadToEnd();
Console.WriteLine($"File content: {content}");

// Process the content...
} // StreamReader.Dispose() is automatically called here

Console.WriteLine("File processing completed successfully");
}
catch (Exception ex)
{
Console.WriteLine($"Error processing file: {ex.Message}");
}

// No finally block needed for resource cleanup when using the 'using' statement
}

Using Declaration (C# 8.0+)​

In C# 8.0 and later, you can use a simplified form of the using statement called a "using declaration":

public void ProcessFileWithUsingDeclaration(string filePath)
{
try
{
// Using declaration - the StreamReader will be disposed at the end of the enclosing scope
using StreamReader reader = new StreamReader(filePath);

string content = reader.ReadToEnd();
Console.WriteLine($"File content: {content}");

// Process the content...
}
catch (Exception ex)
{
Console.WriteLine($"Error processing file: {ex.Message}");
}
// reader.Dispose() is automatically called at the end of the scope
}
Best Practices for Basic Exception Handling
  1. Be specific with catch blocks - Catch specific exception types before more general ones.
  2. Only catch exceptions you can handle - Don't catch exceptions if you can't take meaningful action.
  3. Use the using statement for disposable resources to ensure proper cleanup.
  4. Keep try blocks as small as possible - Include only code that might throw exceptions you want to catch.
  5. Don't swallow exceptions - Always log or report exceptions in some way.
  6. Consider rethrowing important exceptions - Use throw; (not throw ex;) to preserve the original stack trace.

2.7.2 - Advanced Exception Handling Techniques​

Employing advanced techniques for handling exceptions can further enhance the resilience and maintainability of applications. This section covers more sophisticated approaches to exception handling in C#.

2.7.2.1 - Exception Filters (C# 6.0+)​

Exception filters allow you to specify additional conditions for catching exceptions using the when keyword. This feature helps you create more precise exception handlers without nesting try-catch blocks.

/// <summary>
/// Demonstrates exception filters with the 'when' clause
/// </summary>
public void DemonstrateExceptionFilters()
{
try
{
// Get user input
Console.Write("Enter a number: ");
string input = Console.ReadLine();

// Try to parse the input
int number = int.Parse(input);

// Perform some operations based on the input
int result = 100 / number;
Console.WriteLine($"Result: {result}");
}
catch (FormatException ex) when (DateTime.Now.DayOfWeek == DayOfWeek.Monday)
{
// This handler only executes on Mondays
Console.WriteLine("Monday blues! Please enter a valid number.");
Console.WriteLine($"Exception details: {ex.Message}");
}
catch (FormatException ex)
{
// This handler executes on other days
Console.WriteLine("Please enter a valid number.");
Console.WriteLine($"Exception details: {ex.Message}");
}
catch (DivideByZeroException ex) when (Debugger.IsAttached)
{
// This handler only executes when a debugger is attached
Console.WriteLine("Debugger detected! Division by zero occurred.");
Console.WriteLine($"Exception details: {ex.Message}");

// You might want to break into the debugger here
Debugger.Break();
}
catch (DivideByZeroException ex)
{
// This handler executes when no debugger is attached
Console.WriteLine("Cannot divide by zero. Please enter a non-zero number.");
Console.WriteLine($"Exception details: {ex.Message}");
}
catch (Exception ex) when (Log(ex))
{
// This handler will never actually execute because Log() always returns false
// But the exception will be logged due to the filter expression
Console.WriteLine("This line will never be reached.");
}
catch (Exception ex)
{
// General exception handler
Console.WriteLine($"An unexpected error occurred: {ex.Message}");
}
}

/// <summary>
/// Logs an exception and always returns false to ensure the catch block doesn't execute
/// </summary>
/// <param name="ex">The exception to log</param>
/// <returns>Always false</returns>
private bool Log(Exception ex)
{
// Log the exception details
Console.WriteLine($"LOGGING: {DateTime.Now} - Exception: {ex.GetType().Name} - {ex.Message}");

// Return false so the catch block doesn't execute
return false;
}

2.7.2.2 - Custom Exceptions​

Creating custom exception types allows you to provide more specific information about errors in your application domain. Custom exceptions should derive from Exception or a more specific exception class.

/// <summary>
/// Custom exception for user-related validation errors
/// </summary>
public class UserValidationException : Exception
{
/// <summary>
/// Gets the name of the user that caused the validation error
/// </summary>
public string Username { get; }

/// <summary>
/// Gets the field that failed validation
/// </summary>
public string FieldName { get; }

/// <summary>
/// Creates a new UserValidationException with the specified error message
/// </summary>
/// <param name="message">The error message</param>
public UserValidationException(string message)
: base(message)
{
}

/// <summary>
/// Creates a new UserValidationException with the specified error message and inner exception
/// </summary>
/// <param name="message">The error message</param>
/// <param name="innerException">The inner exception that caused this exception</param>
public UserValidationException(string message, Exception innerException)
: base(message, innerException)
{
}

/// <summary>
/// Creates a new UserValidationException with detailed user and field information
/// </summary>
/// <param name="message">The error message</param>
/// <param name="username">The username that caused the validation error</param>
/// <param name="fieldName">The field that failed validation</param>
public UserValidationException(string message, string username, string fieldName)
: base(message)
{
Username = username;
FieldName = fieldName;
}

/// <summary>
/// Gets a string representation of this exception including the username and field name
/// </summary>
/// <returns>A string containing exception details</returns>
public override string ToString()
{
string baseString = base.ToString();

if (Username != null && FieldName != null)
{
return $"{baseString}\nUsername: {Username}, Field: {FieldName}";
}

return baseString;
}
}

/// <summary>
/// Demonstrates the use of custom exceptions for domain-specific error handling
/// </summary>
public class UserService
{
/// <summary>
/// Validates and registers a new user
/// </summary>
/// <param name="username">The username to register</param>
/// <param name="email">The user's email address</param>
/// <param name="age">The user's age</param>
/// <exception cref="UserValidationException">Thrown when user data is invalid</exception>
public void RegisterUser(string username, string email, int age)
{
// Validate username
if (string.IsNullOrWhiteSpace(username))
{
throw new UserValidationException(
"Username cannot be empty.",
username,
"Username");
}

if (username.Length < 3)
{
throw new UserValidationException(
"Username must be at least 3 characters long.",
username,
"Username");
}

// Validate email
if (string.IsNullOrWhiteSpace(email))
{
throw new UserValidationException(
"Email cannot be empty.",
username,
"Email");
}

if (!email.Contains("@") || !email.Contains("."))
{
throw new UserValidationException(
"Email format is invalid.",
username,
"Email");
}

// Validate age
if (age < 13)
{
throw new UserValidationException(
"Users must be at least 13 years old.",
username,
"Age");
}

// If all validations pass, register the user
Console.WriteLine($"User {username} registered successfully!");
}
}

/// <summary>
/// Example of using the custom exception
/// </summary>
public void DemonstrateCustomExceptions()
{
UserService userService = new UserService();

try
{
// This will throw a UserValidationException
userService.RegisterUser("jo", "not-an-email", 10);
}
catch (UserValidationException ex) when (ex.FieldName == "Username")
{
Console.WriteLine($"Username validation failed: {ex.Message}");
Console.WriteLine($"Please choose a different username.");
}
catch (UserValidationException ex) when (ex.FieldName == "Email")
{
Console.WriteLine($"Email validation failed: {ex.Message}");
Console.WriteLine($"Please provide a valid email address.");
}
catch (UserValidationException ex) when (ex.FieldName == "Age")
{
Console.WriteLine($"Age validation failed: {ex.Message}");
Console.WriteLine($"Sorry, you must be at least 13 years old to register.");
}
catch (UserValidationException ex)
{
// Catch any other UserValidationException
Console.WriteLine($"Validation failed: {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"An unexpected error occurred: {ex.Message}");
}
}

2.7.2.3 - Exception Propagation and Rethrowing​

Understanding how exceptions propagate through the call stack and how to properly rethrow them is crucial for maintaining the original exception context.

/// <summary>
/// Demonstrates exception propagation through multiple method calls
/// </summary>
public void DemonstrateExceptionPropagation()
{
try
{
// Call a method that might throw an exception
ProcessData("invalid-data");
}
catch (Exception ex)
{
// By the time the exception reaches here, it will have information
// about the entire call stack where the exception propagated
Console.WriteLine("Exception caught at the top level:");
Console.WriteLine($"Message: {ex.Message}");
Console.WriteLine($"Stack trace:\n{ex.StackTrace}");

// We can also check if there are inner exceptions
if (ex.InnerException != null)
{
Console.WriteLine($"Inner exception: {ex.InnerException.Message}");
}
}
}

/// <summary>
/// Processes data and calls other methods
/// </summary>
/// <param name="data">The data to process</param>
private void ProcessData(string data)
{
try
{
// Call another method that might throw an exception
ValidateData(data);

// If validation passes, continue processing
Console.WriteLine($"Processing data: {data}");
}
catch (FormatException ex)
{
// Log the exception
Console.WriteLine($"Data format error in ProcessData: {ex.Message}");

// Rethrow the exception - this preserves the original stack trace
throw;

// DON'T do this - it loses the original stack trace:
// throw ex;
}
catch (Exception ex)
{
// Wrap the exception with additional context
throw new InvalidOperationException(
$"Error processing data '{data}'.", ex);
}
}

/// <summary>
/// Validates data format
/// </summary>
/// <param name="data">The data to validate</param>
/// <exception cref="FormatException">Thrown when data format is invalid</exception>
private void ValidateData(string data)
{
if (data == null)
{
throw new ArgumentNullException(nameof(data), "Data cannot be null.");
}

if (!data.All(char.IsLetterOrDigit))
{
throw new FormatException(
$"Data '{data}' contains invalid characters. Only letters and digits are allowed.");
}

Console.WriteLine($"Data '{data}' is valid.");
}

2.7.2.4 - Exception Data Dictionary​

The Exception.Data property provides a dictionary that can be used to store additional information about an exception. This is useful for adding context without creating custom exception types.

/// <summary>
/// Demonstrates using the Exception.Data dictionary to add context to exceptions
/// </summary>
public void DemonstrateExceptionData()
{
try
{
// Simulate a database operation
SimulateDatabaseOperation("users", "SELECT * FROM users WHERE id = @id");
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");

// Display all the additional data stored in the exception
Console.WriteLine("Additional context:");
foreach (DictionaryEntry entry in ex.Data)
{
Console.WriteLine($" {entry.Key}: {entry.Value}");
}
}
}

/// <summary>
/// Simulates a database operation that might fail
/// </summary>
/// <param name="tableName">The database table name</param>
/// <param name="sqlQuery">The SQL query to execute</param>
private void SimulateDatabaseOperation(string tableName, string sqlQuery)
{
try
{
// Simulate a database connection error
throw new InvalidOperationException("Database connection failed.");
}
catch (Exception ex)
{
// Add contextual information to the exception
ex.Data["Timestamp"] = DateTime.Now;
ex.Data["Table"] = tableName;
ex.Data["Query"] = sqlQuery;
ex.Data["User"] = Environment.UserName;
ex.Data["Machine"] = Environment.MachineName;

// Rethrow the enriched exception
throw;
}
}

2.7.2.5 - Async Exception Handling​

When working with asynchronous code, exception handling requires special attention. Exceptions in async methods are captured and placed in the returned Task.

/// <summary>
/// Demonstrates exception handling in async methods
/// </summary>
public async Task DemonstrateAsyncExceptionHandlingAsync()
{
try
{
// Call an async method that might throw
await ProcessFileAsync("nonexistent.txt");
}
catch (FileNotFoundException ex)
{
Console.WriteLine($"File not found: {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
}

/// <summary>
/// Asynchronously processes a file
/// </summary>
/// <param name="filePath">Path to the file</param>
/// <returns>A task representing the asynchronous operation</returns>
private async Task ProcessFileAsync(string filePath)
{
// The exception will be captured in the returned Task
using StreamReader reader = new StreamReader(filePath);
string content = await reader.ReadToEndAsync();

Console.WriteLine($"File content: {content}");
}
Best Practices for Advanced Exception Handling
  1. Create custom exceptions for domain-specific error conditions to provide more meaningful context.
  2. Use exception filters to create more precise exception handlers without nesting try-catch blocks.
  3. Preserve the original stack trace when rethrowing exceptions by using throw; instead of throw ex;.
  4. Add context to exceptions using the Exception.Data dictionary or by wrapping them in more specific exceptions.
  5. Handle exceptions at the appropriate level - catch exceptions where you have enough context to handle them properly.
  6. Document exceptions in method XML comments using the <exception> tag to inform callers about potential exceptions.
  7. Consider performance implications - exception handling has a performance cost, so use it for exceptional conditions, not for normal control flow.

2.7.3 - Best Practices for Exception Handling​

Effective exception handling is crucial for building robust, maintainable applications. Here are comprehensive best practices to follow when implementing exception handling in your C# code:

2.7.3.1 - General Exception Handling Guidelines​

  1. Only catch exceptions you can handle meaningfully

    • Don't catch exceptions if you can't take appropriate action to recover or provide useful feedback.
    • Let exceptions propagate up the call stack to a level where they can be properly handled.
  2. Use specific exception types

    • Catch specific exception types before more general ones.
    • Avoid catching System.Exception except at application boundaries or for logging purposes.
    // Good practice - catching specific exceptions first
    try
    {
    // Code that might throw exceptions
    }
    catch (ArgumentNullException ex)
    {
    // Handle null argument
    }
    catch (ArgumentException ex)
    {
    // Handle other invalid arguments
    }
    catch (IOException ex)
    {
    // Handle I/O errors
    }
    catch (Exception ex)
    {
    // Last resort - log and report unexpected errors
    LogError(ex);
    ShowErrorToUser("An unexpected error occurred.");
    }
  3. Keep try blocks as small as possible

    • Include only code that might throw exceptions you want to catch.
    • This makes it easier to identify which operation caused the exception.
  4. Always clean up resources

    • Use finally blocks or using statements to ensure resources are properly released.
    • Prefer using statements for disposable resources when possible.
  5. Don't use exceptions for normal flow control

    • Exceptions should be used for exceptional conditions, not for expected scenarios.
    • Using exceptions for normal control flow leads to poor performance and less readable code.
    // Bad practice - using exceptions for control flow
    try
    {
    int value = dictionary["key"];
    // Process value
    }
    catch (KeyNotFoundException)
    {
    // Handle missing key
    }

    // Good practice - check before accessing
    if (dictionary.TryGetValue("key", out int value))
    {
    // Process value
    }
    else
    {
    // Handle missing key
    }

2.7.3.2 - Exception Design and Creation​

  1. Create custom exceptions for domain-specific errors

    • Derive from the most appropriate existing exception class.
    • Include constructors that follow the standard exception pattern.
    • Add properties that provide additional context about the error.
  2. Use meaningful exception messages

    • Include enough information to understand what went wrong.
    • Consider including parameter names, values, and expected ranges.
    • Avoid exposing sensitive information in exception messages.
  3. Document exceptions in method XML comments

    • Use the <exception> tag to document exceptions that might be thrown.
    • Specify the conditions under which each exception is thrown.
    /// <summary>
    /// Withdraws money from an account
    /// </summary>
    /// <param name="accountId">The account ID</param>
    /// <param name="amount">The amount to withdraw</param>
    /// <exception cref="ArgumentException">Thrown when amount is negative or zero</exception>
    /// <exception cref="AccountNotFoundException">Thrown when the account doesn't exist</exception>
    /// <exception cref="InsufficientFundsException">Thrown when the account has insufficient funds</exception>
    public void WithdrawMoney(string accountId, decimal amount)
    {
    // Implementation
    }

2.7.3.3 - Exception Propagation and Handling​

  1. Preserve the original stack trace when rethrowing

    • Use throw; instead of throw ex; to preserve the original stack trace.
    try
    {
    // Code that might throw
    }
    catch (Exception ex)
    {
    // Log the exception
    Logger.Log(ex);

    // Rethrow preserving the original stack trace
    throw; // Correct way to rethrow

    // Don't do this - it resets the stack trace:
    // throw ex;
    }
  2. Add context to exceptions when rethrowing

    • Use the Exception.Data dictionary to add contextual information.
    • Or wrap the original exception in a more specific one using the inner exception parameter.
  3. Handle exceptions at the appropriate level

    • Low-level components should generally let exceptions propagate.
    • Higher-level components should catch and handle exceptions appropriately.
    • Application boundaries (UI, API endpoints, etc.) should catch all exceptions to prevent crashes.
  4. Log exceptions with sufficient context

    • Include the exception type, message, and stack trace.
    • Add contextual information like the current user, operation being performed, etc.
    • Consider logging inner exceptions as well.

2.7.3.4 - Performance and Security Considerations​

  1. Be aware of performance implications

    • Exception handling has a performance cost, especially when exceptions are thrown.
    • Use validation and checks to avoid throwing exceptions in performance-critical code.
  2. Avoid exposing sensitive information

    • Don't include sensitive data (passwords, connection strings, etc.) in exception messages.
    • Consider sanitizing exception details before displaying them to users.
  3. Handle security-related exceptions carefully

    • Don't reveal too much information about security failures.
    • Log security exceptions for audit purposes.
    • Consider using generic error messages for authentication/authorization failures.
  4. Use exception filters for side effects

    • Exception filters (when clause) can be used for logging without catching the exception.
    try
    {
    // Code that might throw
    }
    catch (Exception ex) when (LogException(ex))
    {
    // This block never executes if LogException returns false
    }

    private bool LogException(Exception ex)
    {
    Logger.Log(ex);
    return false; // Don't catch the exception, just log it
    }

By following these best practices, you'll create more robust, maintainable, and user-friendly applications that handle errors gracefully while providing meaningful feedback to users and developers.