Skip to main content

C# Exception Handling Patterns

Exception handling is a critical aspect of writing robust C# applications. While the basic try-catch-finally blocks form the foundation of exception handling, there are several patterns and best practices that can help you write more effective, maintainable, and elegant error-handling code.

Introduction to Exception Handling Patterns​

Exception handling patterns are established solutions to common error-handling scenarios. They help you:

  • Write more maintainable code
  • Improve application robustness
  • Prevent resource leaks
  • Provide better diagnostics
  • Improve user experience during error conditions

Let's explore several key patterns that every C# developer should know.

Basic Try-Catch Pattern​

The fundamental pattern is the try-catch block:

csharp
try
{
// Code that might throw an exception
int result = 10 / 0; // Will throw DivideByZeroException
}
catch (Exception ex)
{
// Handle the exception
Console.WriteLine($"An error occurred: {ex.Message}");
}

Output:

An error occurred: Attempted to divide by zero.

This is the starting point for all exception handling patterns.

Specific to General Exception Handling Pattern​

When catching multiple exception types, always order them from most specific to most general:

csharp
try
{
string text = File.ReadAllText("nonexistent.txt");
int number = int.Parse(text);
}
catch (FileNotFoundException ex)
{
// Handle missing file specifically
Console.WriteLine($"The file was not found: {ex.Message}");
}
catch (FormatException ex)
{
// Handle parsing errors specifically
Console.WriteLine($"The file content couldn't be parsed as a number: {ex.Message}");
}
catch (Exception ex)
{
// Handle any other exceptions generally
Console.WriteLine($"An unexpected error occurred: {ex.Message}");
}

If you put the general Exception catch block first, it would catch all exceptions, and the more specific handlers would never execute.

Try-Catch-Finally Pattern​

Use finally to ensure cleanup code always runs:

csharp
FileStream file = null;
try
{
file = new FileStream("data.txt", FileMode.Open);
// Work with the file...
}
catch (IOException ex)
{
Console.WriteLine($"Error working with the file: {ex.Message}");
}
finally
{
// This code always runs, whether an exception occurred or not
if (file != null)
{
file.Dispose();
}
}

Using Statement Pattern​

The using statement is a cleaner way to ensure proper resource disposal:

csharp
try
{
using (var file = new FileStream("data.txt", FileMode.Open))
{
// Work with the file...
} // file.Dispose() automatically called here
}
catch (IOException ex)
{
Console.WriteLine($"Error accessing the file: {ex.Message}");
}

In C# 8.0 and later, you can use the simplified using declaration:

csharp
try
{
using var file = new FileStream("data.txt", FileMode.Open);
// Work with the file...
} // file.Dispose() automatically called when leaving scope
catch (IOException ex)
{
Console.WriteLine($"Error accessing the file: {ex.Message}");
}

Exception Filtering Pattern​

C# 6.0 introduced exception filters, allowing you to add conditions to your catch blocks:

csharp
try
{
ProcessData("sample.txt");
}
catch (IOException ex) when (ex.Message.Contains("access denied"))
{
Console.WriteLine("You don't have permission to access this file.");
}
catch (IOException ex) when (ex.HResult == -2147024894) // File not found
{
Console.WriteLine("The specified file could not be found.");
}
catch (IOException ex)
{
Console.WriteLine($"A different IO error occurred: {ex.Message}");
}

This pattern keeps your code cleaner than using nested if statements within catch blocks.

Retry Pattern​

For transient errors (like network issues), the retry pattern can be useful:

csharp
public void OperationWithRetry(Action operation)
{
int maxRetries = 3;
int retryCount = 0;
int delayBetweenRetriesMs = 1000; // 1 second

while (true)
{
try
{
operation();
break; // Success! Exit the retry loop
}
catch (Exception ex) when (IsTransientError(ex) && retryCount < maxRetries)
{
retryCount++;
Console.WriteLine($"Operation failed, retry {retryCount} of {maxRetries}");
Thread.Sleep(delayBetweenRetriesMs);
}
catch (Exception ex)
{
// Non-transient error or max retries reached
Console.WriteLine($"Operation failed permanently: {ex.Message}");
throw; // Re-throw the exception
}
}
}

private bool IsTransientError(Exception ex)
{
// Logic to determine if an error is transient
// For example, check if it's a network timeout
return ex is TimeoutException ||
(ex is HttpRequestException && ex.Message.Contains("timeout"));
}

Usage example:

csharp
try
{
OperationWithRetry(() => {
// Some operation that might have transient failures
WebClient client = new WebClient();
string data = client.DownloadString("https://api.example.com/data");
Console.WriteLine("Data downloaded successfully!");
});
}
catch (Exception ex)
{
Console.WriteLine("All retries failed!");
}

Fail Fast Pattern​

For unrecoverable errors, fail fast to avoid further damage:

csharp
public void ProcessCriticalData(string data)
{
if (string.IsNullOrEmpty(data))
{
throw new ArgumentNullException(nameof(data), "Critical data cannot be null or empty!");
}

try
{
// Process data
var result = JsonConvert.DeserializeObject<CriticalData>(data);

if (result == null || !result.IsValid())
{
throw new InvalidDataException("Critical data is not valid!");
}

// Continue with valid data
ProcessValidData(result);
}
catch (JsonException ex)
{
// Log detailed info but don't try to recover
Logger.LogError($"Critical data parsing failed: {ex}");
throw; // Re-throw to stop execution
}
}

Exception Wrapping/Translation Pattern​

This pattern preserves the original exception while providing more context:

csharp
public Customer GetCustomerById(int customerId)
{
try
{
// Database access code
return _repository.FindCustomer(customerId);
}
catch (DbException ex)
{
// Wrap low-level DB exception in a more meaningful exception
throw new CustomerNotFoundException(
$"Customer with ID {customerId} could not be found", ex);
}
}

Global Exception Handler Pattern​

For application-wide exception handling (especially in web apps):

csharp
// Program.cs in an ASP.NET Core application
var builder = WebApplication.CreateBuilder(args);
// Add services...

var app = builder.Build();

// Global exception handling middleware
app.UseExceptionHandler(errorApp => {
errorApp.Run(async context => {
context.Response.StatusCode = 500;
context.Response.ContentType = "application/json";

var exceptionHandlerPathFeature =
context.Features.Get<IExceptionHandlerPathFeature>();

var exception = exceptionHandlerPathFeature?.Error;

// Log the exception
var logger = context.RequestServices.GetRequiredService<ILogger<Program>>();
logger.LogError(exception, "Unhandled exception");

// Return a generic error message to the client
await context.Response.WriteAsync(
JsonSerializer.Serialize(new {
error = "An unexpected error occurred."
}));
});
});

// Other middleware and app configuration...
app.Run();

Custom Exception Class Pattern​

Creating custom exceptions improves code clarity:

csharp
public class OrderProcessingException : Exception
{
public string OrderId { get; }

public OrderProcessingException(string orderId, string message)
: base(message)
{
OrderId = orderId;
}

public OrderProcessingException(string orderId, string message, Exception innerException)
: base(message, innerException)
{
OrderId = orderId;
}
}

// Usage
public void ProcessOrder(Order order)
{
try
{
// Process the order
if (!ValidateOrder(order))
{
throw new OrderProcessingException(
order.Id,
"Order validation failed: Missing required fields.");
}

// Continue processing...
}
catch (DatabaseException ex)
{
throw new OrderProcessingException(
order.Id,
"Database error while processing order",
ex);
}
}

Exception Logging Pattern​

Proper exception logging is crucial for debugging production issues:

csharp
try
{
// Code that might throw
ProcessComplexOperation();
}
catch (Exception ex)
{
// Log all important details
Logger.LogError(
ex,
"Error occurred during {Operation} at {Time}. User: {UserId}",
"ComplexOperation",
DateTime.UtcNow,
currentUser.Id);

// Optional: rethrow or return error to user
throw;
}

Real-World Example: Full E-Commerce Order Processing​

Let's see how multiple patterns can be combined in a realistic scenario:

csharp
public class OrderService
{
private readonly IOrderRepository _repository;
private readonly IPaymentGateway _paymentGateway;
private readonly ILogger<OrderService> _logger;

public OrderService(
IOrderRepository repository,
IPaymentGateway paymentGateway,
ILogger<OrderService> logger)
{
_repository = repository;
_paymentGateway = paymentGateway;
_logger = logger;
}

public async Task<OrderResult> PlaceOrderAsync(Order order)
{
// Fail fast pattern
if (order == null || !order.IsValid())
{
throw new ArgumentException("Invalid order data", nameof(order));
}

try
{
// Save the order
await _repository.SaveOrderAsync(order);

// Retry pattern for payment processing
await ProcessPaymentWithRetryAsync(order);

// Complete the order
order.Status = OrderStatus.Completed;
await _repository.UpdateOrderAsync(order);

return new OrderResult { Success = true, OrderId = order.Id };
}
catch (PaymentDeclinedException ex)
{
// Handle specific payment failure
_logger.LogWarning(ex, "Payment declined for order {OrderId}", order.Id);

// Update order status
order.Status = OrderStatus.PaymentFailed;
await _repository.UpdateOrderAsync(order);

return new OrderResult
{
Success = false,
OrderId = order.Id,
ErrorMessage = "Your payment was declined. Please try another payment method."
};
}
catch (Exception ex)
{
// Exception wrapping pattern
_logger.LogError(ex, "Failed to process order {OrderId}", order.Id);

// Try to mark the order as failed
try
{
order.Status = OrderStatus.Error;
await _repository.UpdateOrderAsync(order);
}
catch (Exception updateEx)
{
_logger.LogError(updateEx, "Failed to update failed order status");
}

throw new OrderProcessingException(
order.Id,
"An unexpected error occurred while processing your order",
ex);
}
}

private async Task ProcessPaymentWithRetryAsync(Order order)
{
int maxRetries = 3;
int retryCount = 0;
TimeSpan delay = TimeSpan.FromSeconds(2);

while (true)
{
try
{
// Attempt payment
await _paymentGateway.ProcessPaymentAsync(order.Payment);
break; // Success
}
catch (Exception ex) when (IsTransientError(ex) && retryCount < maxRetries)
{
retryCount++;
_logger.LogWarning(
ex,
"Transient error during payment processing (attempt {RetryCount}/{MaxRetries})",
retryCount, maxRetries);

await Task.Delay(delay);
delay = TimeSpan.FromMilliseconds(delay.TotalMilliseconds * 2); // Exponential backoff
}
}
}

private bool IsTransientError(Exception ex)
{
return ex is TimeoutException ||
ex is HttpRequestException ||
(ex is PaymentGatewayException pge && pge.IsTransient);
}
}

Summary​

Mastering these exception handling patterns will significantly improve the quality of your C# code. Remember these key points:

  • Always catch specific exceptions before general ones
  • Use finally blocks or using statements for resource cleanup
  • Apply exception filters for cleaner code
  • Implement retry logic for transient errors
  • Create custom exceptions for better semantics
  • Log exceptions with sufficient context
  • Consider global exception handling for application-wide concerns

By combining these patterns appropriately, you'll write more robust, maintainable C# applications.

Additional Resources​

Exercises​

  1. Implement a file processing application that demonstrates at least three exception handling patterns.
  2. Create a web service client with retry logic for handling network failures.
  3. Design custom exceptions for a banking application that properly encapsulate domain-specific error information.
  4. Implement global exception handling for a web API that returns appropriate HTTP status codes.
  5. Write a utility class that safely executes arbitrary code with proper exception handling and logging.


If you spot any mistakes on this website, please let me know at feedback@compilenrun.com. I’d greatly appreciate your feedback! :)