.NET Error Handling Strategies
Effective error handling is a critical aspect of writing robust .NET applications. When your code encounters unexpected situations, proper error handling ensures your application degrades gracefully rather than crashing. In this guide, we'll explore various error handling strategies in .NET, helping you build more reliable applications.
Introduction to Error Handling
Error handling in .NET primarily revolves around the exception handling mechanism. Exceptions represent errors that occur during application execution, disrupting the normal flow of code. Instead of letting these exceptions crash your application, you can anticipate and handle them appropriately.
Why Error Handling Matters
- Improved User Experience: Users see helpful messages instead of cryptic errors
- Application Stability: Your application continues running despite errors
- Maintainability: Structured error handling makes code easier to debug
- Security: Prevents exposing sensitive information through error messages
Basic Exception Handling: The Try-Catch Block
The fundamental mechanism for handling errors in .NET is the try-catch block.
Basic Syntax
try
{
// Code that might throw an exception
int result = 10 / 0; // This will throw a DivideByZeroException
}
catch (Exception ex)
{
// Code to handle the exception
Console.WriteLine($"An error occurred: {ex.Message}");
}
Output:
An error occurred: Attempted to divide by zero.
Catching Specific Exceptions
It's better practice to catch specific exceptions rather than generic ones:
try
{
int[] numbers = new int[5];
numbers[10] = 5; // This will throw an IndexOutOfRangeException
}
catch (IndexOutOfRangeException ex)
{
Console.WriteLine($"Array index error: {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"General error: {ex.Message}");
}
Output:
Array index error: Index was outside the bounds of the array.
Adding Finally Block
The finally
block contains code that runs regardless of whether an exception occurred:
FileStream file = null;
try
{
file = File.Open("data.txt", FileMode.Open);
// Process file...
}
catch (FileNotFoundException ex)
{
Console.WriteLine($"File not found: {ex.Message}");
}
finally
{
// This code runs whether an exception occurred or not
file?.Dispose();
Console.WriteLine("File resources have been cleaned up");
}
Advanced Exception Handling Techniques
Exception Filters (C# 6+)
Exception filters allow you to specify conditions for when to handle exceptions:
try
{
ProcessData("data.txt");
}
catch (Exception ex) when (ex.Message.Contains("file not found"))
{
Console.WriteLine("Please check the file name and try again.");
}
catch (Exception ex) when (ex.InnerException != null)
{
Console.WriteLine($"Nested error occurred: {ex.InnerException.Message}");
}
Throwing Exceptions
Sometimes you need to throw exceptions yourself:
public void ProcessAge(int age)
{
if (age < 0)
{
throw new ArgumentException("Age cannot be negative");
}
// Continue processing...
Console.WriteLine($"Processing age: {age}");
}
To use:
try
{
ProcessAge(-5);
}
catch (ArgumentException ex)
{
Console.WriteLine($"Input error: {ex.Message}");
}
Output:
Input error: Age cannot be negative
Custom Exceptions
Creating custom exceptions helps make your error handling more specific:
public class UserNotFoundException : Exception
{
public string Username { get; }
public UserNotFoundException(string username)
: base($"User '{username}' was not found in the system")
{
Username = username;
}
public UserNotFoundException(string username, Exception innerException)
: base($"User '{username}' was not found in the system", innerException)
{
Username = username;
}
}
// Using the custom exception
public User GetUser(string username)
{
User user = _userRepository.FindByUsername(username);
if (user == null)
{
throw new UserNotFoundException(username);
}
return user;
}
Error Handling Strategies for Different Application Types
Console Applications
static void Main(string[] args)
{
try
{
// Application logic here
RunApplication(args);
}
catch (Exception ex)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"Error: {ex.Message}");
Console.ResetColor();
// Log the full exception details
LogException(ex);
return 1; // Exit code indicating error
}
return 0; // Success exit code
}
Web API Error Handling
In ASP.NET Core Web API applications, you can create a global exception handler:
// In Startup.cs or Program.cs
app.UseExceptionHandler(errorApp =>
{
errorApp.Run(async context =>
{
context.Response.StatusCode = 500;
context.Response.ContentType = "application/json";
var exceptionHandlerFeature = context.Features.Get<IExceptionHandlerFeature>();
if (exceptionHandlerFeature != null)
{
var exception = exceptionHandlerFeature.Error;
// Log the exception
logger.LogError(exception, "Unhandled exception");
// Return a custom error response
var response = new
{
StatusCode = 500,
Message = "An unexpected error occurred",
DetailedMessage = exception.Message
};
await context.Response.WriteAsJsonAsync(response);
}
});
});
Best Practices for Error Handling
-
Don't catch exceptions you can't handle properly
csharp// Bad practice
try {
// Some code
}
catch (Exception) {
// Empty catch block - errors are silently ignored!
}
// Good practice
try {
// Some code
}
catch (Exception ex) {
// Log the error
logger.LogError(ex, "Operation failed");
// Take appropriate action or rethrow if necessary
} -
Avoid exception handling for flow control
csharp// Bad practice - using exceptions for flow control
try {
var user = users.First(u => u.Username == username);
return user;
}
catch (InvalidOperationException) {
return null; // User not found
}
// Good practice - use normal flow control
var user = users.FirstOrDefault(u => u.Username == username);
return user; // Will return null if not found -
Include contextual information in exceptions
csharp// Less helpful
throw new Exception("Database error");
// More helpful
throw new InvalidOperationException(
$"Failed to save order {orderId} to database. Connection string: {maskedConnectionString}",
innerException); -
Log exceptions with appropriate severity levels
csharptry {
ProcessOrder(order);
}
catch (ValidationException ex) {
// Business rule violation - not critical
logger.LogWarning(ex, "Order validation failed");
return BadRequest(new { Message = ex.Message });
}
catch (Exception ex) {
// Unexpected exception - critical issue
logger.LogError(ex, "Order processing failed unexpectedly");
return StatusCode(500, new { Message = "An unexpected error occurred" });
}
Real-World Example: Resilient File Processing
Here's a complete example showing robust error handling for a file processing scenario:
public class FileProcessor
{
private readonly ILogger<FileProcessor> _logger;
public FileProcessor(ILogger<FileProcessor> logger)
{
_logger = logger;
}
public ProcessingResult ProcessCustomerFile(string filePath)
{
// Initialize result
var result = new ProcessingResult { FileName = Path.GetFileName(filePath) };
try
{
// Validate file exists
if (!File.Exists(filePath))
{
_logger.LogWarning("Customer file not found: {FilePath}", filePath);
result.Success = false;
result.ErrorMessage = "File not found";
return result;
}
// Check file can be accessed
try
{
// Just test if we can open it
using var stream = File.OpenRead(filePath);
}
catch (UnauthorizedAccessException ex)
{
_logger.LogError(ex, "Permission denied for file: {FilePath}", filePath);
result.Success = false;
result.ErrorMessage = "Permission denied";
return result;
}
// Process file content
try
{
string[] lines = File.ReadAllLines(filePath);
result.RecordsProcessed = ProcessCustomerRecords(lines);
result.Success = true;
_logger.LogInformation("Successfully processed {RecordCount} records from {FilePath}",
result.RecordsProcessed, filePath);
}
catch (FormatException ex)
{
_logger.LogError(ex, "Invalid data format in file: {FilePath}", filePath);
result.Success = false;
result.ErrorMessage = "File contains invalid data";
return result;
}
}
catch (Exception ex)
{
// Catch-all for unexpected errors
_logger.LogCritical(ex, "Unexpected error processing file: {FilePath}", filePath);
result.Success = false;
result.ErrorMessage = "An unexpected error occurred";
// Consider rethrowing for truly exceptional cases
// throw;
}
return result;
}
private int ProcessCustomerRecords(string[] lines)
{
// Processing logic here...
return lines.Length; // Return count of processed records
}
}
public class ProcessingResult
{
public bool Success { get; set; }
public string FileName { get; set; }
public int RecordsProcessed { get; set; }
public string ErrorMessage { get; set; }
}
Using ILogger
for Structured Logging
Modern .NET applications use the built-in logging abstractions:
public class OrderService
{
private readonly ILogger<OrderService> _logger;
public OrderService(ILogger<OrderService> logger)
{
_logger = logger;
}
public async Task<Order> PlaceOrder(OrderRequest request)
{
try
{
_logger.LogInformation("Processing order for customer {CustomerId}", request.CustomerId);
// Business logic here
var order = new Order { /* ... */ };
return order;
}
catch (InventoryException ex)
{
_logger.LogWarning(ex, "Inventory insufficient for order {OrderId}", request.OrderId);
throw; // Rethrow as this is an expected business exception
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to process order {OrderId} for customer {CustomerId}",
request.OrderId, request.CustomerId);
throw new OrderProcessingException("Order could not be processed", ex);
}
}
}
Summary
Effective error handling is a crucial aspect of building robust .NET applications. In this guide, we've covered:
- Basic try-catch-finally blocks for exception handling
- Advanced techniques like exception filters
- Creating and using custom exceptions
- Strategies for different application types
- Best practices for maintainable error handling
- Real-world examples demonstrating comprehensive error handling
By implementing these strategies, your .NET applications will be more resilient, easier to debug, and provide better user experiences when things go wrong.
Additional Resources
Exercises
- Create a custom exception class for a banking application that handles insufficient funds scenarios
- Implement a global exception handler for an ASP.NET Core Web API
- Write a file processing utility with comprehensive error handling for different failure scenarios
- Practice using exception filters to handle different exception conditions
- Implement a retry mechanism for operations that might fail temporarily (e.g., network calls)
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)