Error Management, commonly referred to as Exception Handling, is the systematic approach to detecting, responding to, and recovering from runtime errors in C# applications. Its core purpose is to gracefully handle exceptional conditions that disrupt normal program flow, preventing application crashes and maintaining system stability.
Try/Catch/Finally
The try-catch-finally block forms the foundation of C# exception handling. The try block contains code that might throw exceptions, catch blocks handle specific exceptions, and the finally block contains cleanup code that always executes.
public class FileProcessor
{
public void ProcessFile(string filePath)
{
StreamReader reader = null;
try
{
// Code that might throw exceptions
reader = new StreamReader(filePath);
string content = reader.ReadToEnd();
Console.WriteLine($"File content: {content}");
}
catch (FileNotFoundException ex)
{
// Handle specific exception type
Console.WriteLine($"File not found: {ex.FileName}");
}
catch (IOException ex)
{
// Handle IO-related exceptions
Console.WriteLine($"IO error: {ex.Message}");
}
catch (Exception ex)
{
// Catch-all for any other exceptions
Console.WriteLine($"Unexpected error: {ex.Message}");
}
finally
{
// Always execute cleanup code
reader?.Dispose();
Console.WriteLine("Cleanup completed");
}
}
}
Best Practice: Order catch blocks from most specific to least specific exception types.
Custom Exceptions
Custom exceptions allow you to create domain-specific error types that convey meaningful information about application-specific failure conditions. They should inherit from Exception or its derived classes.
// Custom exception for business rule violations
public class InvalidOrderException : Exception
{
public string OrderId { get; }
public string ViolatedRule { get; }
public InvalidOrderException(string orderId, string violatedRule)
: base($"Order {orderId} violated rule: {violatedRule}")
{
OrderId = orderId;
ViolatedRule = violatedRule;
}
// Advanced: Support for serialization
protected InvalidOrderException(
System.Runtime.Serialization.SerializationInfo info,
System.Runtime.Serialization.StreamingContext context) : base(info, context)
{
OrderId = info.GetString(nameof(OrderId));
ViolatedRule = info.GetString(nameof(ViolatedRule));
}
}
public class OrderValidator
{
public void ValidateOrder(Order order)
{
if (order.TotalAmount < 0)
{
// Throw custom exception with domain-specific context
throw new InvalidOrderException(order.Id, "Total amount cannot be negative");
}
if (order.Items.Count == 0)
{
throw new InvalidOrderException(order.Id, "Order must contain at least one item");
}
}
}
Naming Convention: Always suffix custom exception classes with “Exception” for clarity.
Filtering
Exception filtering allows you to catch exceptions based on conditions beyond just the exception type, using the when keyword. This enables more precise exception handling without unwinding the stack.
public class DatabaseService
{
private int retryCount = 0;
private const int MaxRetries = 3;
public void ExecuteQuery(string query)
{
try
{
// Simulate database operation
if (DateTime.Now.Second % 3 == 0) // Simulate intermittent failure
throw new SqlException { Number = 1205 }; // Deadlock
else if (DateTime.Now.Second % 5 == 0)
throw new SqlException { Number = -2 }; // Timeout
Console.WriteLine("Query executed successfully");
retryCount = 0; // Reset on success
}
catch (SqlException ex) when (ex.Number == 1205 && retryCount < MaxRetries)
{
// Filter for deadlock errors with retry logic
retryCount++;
Console.WriteLine($"Deadlock detected, retrying... ({retryCount}/{MaxRetries})");
System.Threading.Thread.Sleep(100); // Brief pause
ExecuteQuery(query); // Retry
}
catch (SqlException ex) when (ex.Number == -2)
{
// Filter for timeout errors - no retry
Console.WriteLine("Query timeout - requires manual intervention");
throw; // Re-throw to caller
}
catch (SqlException ex) when (ShouldRetry(ex))
{
// Complex filtering using helper method
HandleRetryableError(ex);
}
}
private bool ShouldRetry(SqlException ex)
{
// Define retryable error codes
int[] retryableErrors = { 1205, 1222, 8651 };
return retryableErrors.Contains(ex.Number) && retryCount < MaxRetries;
}
private void HandleRetryableError(SqlException ex)
{
retryCount++;
Console.WriteLine($"Retryable error {ex.Number}, attempt {retryCount}");
System.Threading.Thread.Sleep(100);
ExecuteQuery("SELECT * FROM Users");
}
}
Filter Execution: Exception filters execute before stack unwinding, preserving the full call stack for debugging.
Why Error Management is Important
- Resilience Principle: Enables applications to gracefully handle failures and continue operating
- Separation of Concerns (SOLID): Isolates error-handling logic from business logic
- Debugging Efficiency: Provides structured error information and context
Advanced Nuances
AggregateException for Parallel Processing
When working with parallel operations, multiple exceptions can be wrapped in AggregateException:
public void ProcessMultipleFiles(string[] files)
{
try
{
Parallel.ForEach(files, file =>
{
if (file.Contains("invalid"))
throw new ArgumentException($"Invalid file: {file}");
// Process file...
});
}
catch (AggregateException ae)
{
// Handle multiple exceptions from parallel operations
ae.Handle(ex =>
{
if (ex is ArgumentException)
{
Console.WriteLine($"Skipping invalid file: {ex.Message}");
return true; // Exception handled
}
return false; // Exception not handled - will be rethrown
});
}
}
Exception DispatchInfo for Re-throwing
Preserving stack traces when re-throwing exceptions across boundaries:
public void ExceptionPreservationExample()
{
ExceptionDispatchInfo capturedException = null;
try
{
MethodThatThrows();
}
catch (Exception ex)
{
// Capture exception without losing stack trace
capturedException = ExceptionDispatchInfo.Capture(ex);
}
// Later, re-throw with original stack trace
capturedException?.Throw();
}
Roadmap Context
Error Management serves as the foundation of the “Exception Handling” section. It’s a prerequisite for:
- Exception Handling Patterns: Circuit Breaker, Retry, and Fallback patterns
- Global Exception Handling: Application-level error handling strategies
- Performance Considerations: Understanding exception overhead
- Async Exception Handling: Extending concepts to asynchronous programming