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:
- Normal execution stops immediately
- The program looks for a handler that can deal with the error
- If a handler is found, it processes the error
- 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:
- Something goes wrong (the potato gets hot)
- The code throws the hot potato (exception)
- 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 Class | Description | Example Scenario |
---|---|---|
System.Exception | The base class for all exceptions. | Parent of all exceptions |
System.IndexOutOfRangeException | Thrown when attempting to access elements outside the bounds of an array or collection | int[] array = {1, 2, 3}; int item = array[5]; |
System.NullReferenceException | Thrown when trying to use an object reference that has not been initialized | string name = null; int length = name.Length; |
System.InvalidOperationException | Thrown when an operation is attempted that is invalid for the object's current state | Calling a method on an object that's in an invalid state |
System.ArgumentException | Thrown when a method receives an argument that is not valid | Passing a negative number to a method requiring positive values |
System.ArgumentNullException | Thrown when a null argument is passed to a method that doesn't accept it | ProcessData(null) when null isn't allowed |
System.FormatException | Thrown when the format of an argument doesn't meet the parameter specifications | int.Parse("abc") when a number is expected |
System.IO.IOException | Thrown for errors encountered during input/output operations | Attempting to read from a file that is locked |
System.DivideByZeroException | Thrown when an attempt is made to divide an integer by zero | int result = 10 / 0; |
System.OverflowException | Thrown when an arithmetic operation results in an overflow | byte 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:
- A
try
block containing code that might throw exceptions - One or more
catch
blocks that handle specific types of exceptions - 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β
-
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
} -
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:
- The runtime looks for a matching
catch
block in the current method - If no matching
catch
block is found, the exception "bubbles up" to the calling method - This process continues up the call stack until a matching handler is found
- 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
}
- Be specific with catch blocks - Catch specific exception types before more general ones.
- Only catch exceptions you can handle - Don't catch exceptions if you can't take meaningful action.
- Use the
using
statement for disposable resources to ensure proper cleanup. - Keep try blocks as small as possible - Include only code that might throw exceptions you want to catch.
- Don't swallow exceptions - Always log or report exceptions in some way.
- Consider rethrowing important exceptions - Use
throw;
(notthrow 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}");
}
- Create custom exceptions for domain-specific error conditions to provide more meaningful context.
- Use exception filters to create more precise exception handlers without nesting try-catch blocks.
- Preserve the original stack trace when rethrowing exceptions by using
throw;
instead ofthrow ex;
. - Add context to exceptions using the
Exception.Data
dictionary or by wrapping them in more specific exceptions. - Handle exceptions at the appropriate level - catch exceptions where you have enough context to handle them properly.
- Document exceptions in method XML comments using the
<exception>
tag to inform callers about potential exceptions. - 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β
-
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.
-
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.");
} -
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.
-
Always clean up resources
- Use
finally
blocks orusing
statements to ensure resources are properly released. - Prefer
using
statements for disposable resources when possible.
- Use
-
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β
-
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.
-
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.
-
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
} - Use the
2.7.3.3 - Exception Propagation and Handlingβ
-
Preserve the original stack trace when rethrowing
- Use
throw;
instead ofthrow 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;
} - Use
-
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.
- Use the
-
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.
-
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β
-
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.
-
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.
-
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.
-
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
} - Exception filters (
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.