C# Best Practices for Exception Handling
Introduction
Exception handling is a critical aspect of building robust C# applications. While the basic syntax of try-catch-finally
blocks might seem straightforward, implementing effective exception handling requires following established best practices. This guide will walk you through essential best practices for handling exceptions in C#, helping you write code that is more reliable, maintainable, and user-friendly.
Why Good Exception Handling Matters
Poor exception handling can lead to:
- Security vulnerabilities
- Application crashes
- Data corruption
- Confused users
- Difficult debugging and maintenance
By following best practices, you'll avoid these pitfalls and build more professional applications.
Essential Best Practices
1. Only Catch Exceptions You Can Handle
One of the most important rules is to only catch exceptions that you can meaningfully handle.
// ❌ Bad practice: Catching all exceptions
try
{
ProcessFile("data.txt");
}
catch (Exception ex)
{
// Generic handling that doesn't add value
Console.WriteLine("An error occurred");
}
// ✅ Good practice: Catching specific exceptions
try
{
ProcessFile("data.txt");
}
catch (FileNotFoundException ex)
{
Console.WriteLine("The file data.txt was not found. Please check the file path.");
// Take specific recovery action
}
catch (UnauthorizedAccessException ex)
{
Console.WriteLine("You don't have permission to access this file.");
// Take specific recovery action
}
2. Avoid Empty Catch Blocks
Empty catch blocks silently swallow exceptions, making bugs extremely difficult to find.
// ❌ Bad practice: Empty catch block
try
{
int result = DivideNumbers(10, 0);
}
catch (DivideByZeroException)
{
// Empty catch block hides the problem!
}
// ✅ Good practice: At minimum, log the exception
try
{
int result = DivideNumbers(10, 0);
}
catch (DivideByZeroException ex)
{
// Log the error
Logger.LogError(ex, "Division by zero occurred");
// Inform the user
Console.WriteLine("Cannot divide by zero. Please provide a non-zero value.");
}
3. Use Finally Blocks for Cleanup
Always use finally
blocks to release resources and ensure cleanup code runs regardless of exceptions.
FileStream file = null;
try
{
file = new FileStream("data.txt", FileMode.Open);
// Process file contents
}
catch (IOException ex)
{
Console.WriteLine($"Error reading file: {ex.Message}");
}
finally
{
// This runs whether an exception occurred or not
file?.Dispose();
}
4. Use Using Statements for IDisposable Resources
The using
statement provides a cleaner alternative for handling IDisposable
resources.
// ✅ Good practice: Using statement handles disposal automatically
try
{
using (StreamReader reader = new StreamReader("data.txt"))
{
string content = reader.ReadToEnd();
ProcessContent(content);
} // StreamReader is automatically disposed here
}
catch (IOException ex)
{
Console.WriteLine($"Error reading file: {ex.Message}");
}
5. Include Meaningful Exception Messages
When throwing exceptions, include detailed information to help with debugging.
// ❌ Bad practice: Unclear exception message
public void ProcessOrder(Order order)
{
if (order == null)
throw new ArgumentNullException(); // Missing parameter name
}
// ✅ Good practice: Clear, informative exception message
public void ProcessOrder(Order order)
{
if (order == null)
throw new ArgumentNullException(nameof(order),
"Order cannot be null when calling ProcessOrder");
}
6. Create Custom Exceptions for Domain-Specific Errors
For application-specific errors, create custom exception classes.
// Define a custom exception
public class OrderProcessingException : Exception
{
public string OrderId { get; }
public OrderProcessingException(string orderId, string message, Exception innerException)
: base(message, innerException)
{
OrderId = orderId;
}
}
// Using the custom exception
public void ProcessOrder(Order order)
{
try
{
// Order processing logic
ValidateOrder(order);
ChargeCustomer(order);
ShipOrder(order);
}
catch (PaymentException ex)
{
throw new OrderProcessingException(order.Id,
$"Failed to process payment for order {order.Id}", ex);
}
}
7. Avoid Exceptions for Control Flow
Don't use exceptions for normal program flow control.
// ❌ Bad practice: Using exceptions for control flow
public bool DoesFileExist(string path)
{
try
{
using (FileStream fs = File.OpenRead(path))
{
return true;
}
}
catch (FileNotFoundException)
{
return false;
}
}
// ✅ Good practice: Using standard methods for control flow
public bool DoesFileExist(string path)
{
return File.Exists(path);
}
8. Log Exceptions with Contextual Information
When logging exceptions, include context to make troubleshooting easier.
try
{
ProcessOrder(order);
}
catch (Exception ex)
{
// Log with context
logger.LogError(ex,
"Order processing failed. OrderId: {OrderId}, Customer: {CustomerId}, Total: {Total}",
order.Id, order.CustomerId, order.Total);
throw; // Re-throw if you can't handle it here
}
Real-World Example: File Processing Application
Let's see these principles applied in a more complete example:
public class FileProcessor
{
private readonly ILogger _logger;
public FileProcessor(ILogger logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public ProcessingResult ProcessCustomerFile(string filePath, string customerId)
{
// Validate arguments
if (string.IsNullOrWhiteSpace(filePath))
throw new ArgumentException("File path cannot be empty", nameof(filePath));
if (string.IsNullOrWhiteSpace(customerId))
throw new ArgumentException("Customer ID cannot be empty", nameof(customerId));
try
{
// Check if file exists before attempting to process
if (!File.Exists(filePath))
{
return new ProcessingResult
{
Success = false,
ErrorMessage = $"File not found at path: {filePath}"
};
}
// Attempt to parse and process the file
using (StreamReader reader = new StreamReader(filePath))
{
try
{
string content = reader.ReadToEnd();
CustomerData data = ParseCustomerData(content, customerId);
SaveToDatabase(data);
return new ProcessingResult { Success = true };
}
catch (FormatException ex)
{
_logger.LogError(ex,
"Format error while parsing customer file. CustomerId: {CustomerId}, FilePath: {FilePath}",
customerId, filePath);
return new ProcessingResult
{
Success = false,
ErrorMessage = "The file format is invalid. Please check the file contents."
};
}
catch (CustomerDataException ex)
{
_logger.LogError(ex,
"Customer data error in file. CustomerId: {CustomerId}, FilePath: {FilePath}, ErrorField: {Field}",
customerId, filePath, ex.FieldName);
return new ProcessingResult
{
Success = false,
ErrorMessage = $"Invalid customer data: {ex.Message}"
};
}
}
}
catch (UnauthorizedAccessException ex)
{
_logger.LogError(ex,
"Access denied to customer file. CustomerId: {CustomerId}, FilePath: {FilePath}",
customerId, filePath);
return new ProcessingResult
{
Success = false,
ErrorMessage = "You don't have permission to access this file."
};
}
catch (IOException ex)
{
_logger.LogError(ex,
"IO error while processing customer file. CustomerId: {CustomerId}, FilePath: {FilePath}",
customerId, filePath);
return new ProcessingResult
{
Success = false,
ErrorMessage = $"Error reading the file: {ex.Message}"
};
}
catch (Exception ex)
{
// Catch-all for unexpected errors
_logger.LogError(ex,
"Unexpected error processing customer file. CustomerId: {CustomerId}, FilePath: {FilePath}",
customerId, filePath);
// For unexpected errors, we might want to throw rather than return
throw new CustomerProcessingException(
$"Unexpected error processing customer {customerId}", ex);
}
}
// Custom domain-specific exception
public class CustomerDataException : Exception
{
public string FieldName { get; }
public CustomerDataException(string fieldName, string message)
: base(message)
{
FieldName = fieldName;
}
}
// Result class to return structured information
public class ProcessingResult
{
public bool Success { get; set; }
public string ErrorMessage { get; set; }
}
}
// Custom exception for the module
public class CustomerProcessingException : Exception
{
public CustomerProcessingException(string message, Exception innerException)
: base(message, innerException)
{
}
}
Exception Handling in Asynchronous Code
When working with async/await, follow these additional best practices:
// ✅ Good practice for async methods
public async Task<OrderResult> ProcessOrderAsync(Order order)
{
try
{
await ValidateOrderAsync(order);
await ChargeCreditCardAsync(order.Payment);
await CreateShipmentAsync(order);
return new OrderResult { Success = true };
}
catch (PaymentDeclinedException ex)
{
_logger.LogWarning(ex, "Payment declined for order {OrderId}", order.Id);
return new OrderResult
{
Success = false,
ErrorCode = "PAYMENT_DECLINED",
ErrorMessage = "Your payment was declined. Please use a different payment method."
};
}
catch (Exception ex) when (ex is InventoryException || ex is ShippingException)
{
_logger.LogError(ex, "Order fulfillment error for order {OrderId}", order.Id);
return new OrderResult
{
Success = false,
ErrorCode = "FULFILLMENT_ERROR",
ErrorMessage = "We're having trouble processing your order. Please try again later."
};
}
}
Summary
Following these best practices for exception handling in C# will help you create more robust, maintainable, and reliable applications. Remember these key points:
- Only catch exceptions you can meaningfully handle
- Avoid empty catch blocks
- Always clean up resources with
finally
orusing
statements - Include detailed exception messages
- Create custom exceptions for domain-specific errors
- Don't use exceptions for normal control flow
- Log exceptions with contextual information
- Handle exceptions appropriately in asynchronous code
By implementing these practices, you'll build applications that degrade gracefully when errors occur, making them more professional and user-friendly.
Exercises
- Refactor the following code to follow best practices:
public void SaveData(string data)
{
try
{
File.WriteAllText("output.txt", data);
}
catch (Exception)
{
// Ignore all errors
}
}
-
Create a custom exception class for a banking application that represents an "Insufficient Funds" error.
-
Implement proper exception handling for a method that reads a configuration file, including appropriate validation and error messages.
Additional Resources
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)