Skip to main content

C# Exception Filters

Introduction

Exception handling is a critical aspect of writing robust C# applications. While basic try-catch blocks serve most needs, C# 6.0 introduced a powerful feature called exception filters that allows you to conditionally catch exceptions based on custom logic. This feature enhances your exception handling capabilities by providing more granular control over which exceptions to handle.

Exception filters let you specify a condition that must be true for the catch block to execute. If the condition evaluates to false, the exception continues to propagate up the call stack as if that catch block didn't exist.

Basic Syntax of Exception Filters

The syntax for exception filters uses the when keyword followed by a boolean expression:

csharp
try 
{
// Code that might throw an exception
}
catch (ExceptionType ex) when (condition)
{
// This catch block only executes if condition is true
}

The condition can be any expression that returns a boolean value, giving you tremendous flexibility in how you handle exceptions.

Simple Example of Exception Filtering

Let's start with a basic example to understand how exception filters work:

csharp
try
{
int number = int.Parse(Console.ReadLine());
Console.WriteLine($"You entered: {number}");
}
catch (FormatException ex) when (DateTime.Now.DayOfWeek == DayOfWeek.Monday)
{
Console.WriteLine("Format exceptions on Mondays are handled differently!");
Console.WriteLine($"Error: {ex.Message}");
}
catch (FormatException ex)
{
Console.WriteLine($"You didn't enter a valid number: {ex.Message}");
}

In this example:

  • If the user enters a non-numeric value on a Monday, the first catch block executes
  • If the user enters a non-numeric value on any other day, the second catch block executes

The filter allows us to have specialized handling for the same exception type based on external conditions.

Practical Use Cases for Exception Filters

1. Logging Only Specific Exceptions

Exception filters are excellent for selective logging:

csharp
try
{
// Database operation
DatabaseOperation();
}
catch (SqlException ex) when (LogError(ex))
{
// This block executes only if LogError returns true
Console.WriteLine("A database error occurred and has been logged.");
}

// Method that logs the exception and returns true
private static bool LogError(Exception ex)
{
// Log the exception details to a file or monitoring service
File.AppendAllText("error.log", $"{DateTime.Now}: {ex.Message}\n");

// Return true to indicate the exception should be caught
return true;
}

This pattern allows you to log errors without affecting the flow of exception handling. It's especially useful when you want to log all exceptions but only handle specific ones.

2. Filtering Exceptions Based on Error Codes

Many exceptions include specific error codes that can help identify the exact problem:

csharp
try
{
// Attempt to access a file
using var file = File.OpenRead("data.txt");
// Process the file...
}
catch (FileNotFoundException ex) when (ex.FileName.Contains("data"))
{
Console.WriteLine("The data file is missing. Please ensure it exists in the application directory.");
}
catch (IOException ex) when (ex.HResult == -2147024864) // Access denied error code
{
Console.WriteLine("You don't have permission to access this file. Try running as administrator.");
}
catch (IOException ex)
{
Console.WriteLine($"An I/O error occurred: {ex.Message}");
}

This approach lets you provide more specific and helpful error messages to users based on the exact nature of the problem.

3. Conditional Retry Logic

Exception filters can be used with retry mechanisms:

csharp
int retryCount = 0;
const int maxRetries = 3;

while (true)
{
try
{
// Attempt some network operation
MakeNetworkRequest();
break; // Success! Exit the retry loop
}
catch (HttpRequestException ex) when (retryCount < maxRetries)
{
retryCount++;
Console.WriteLine($"Network error: {ex.Message}. Retry attempt {retryCount}");
Task.Delay(1000 * retryCount).Wait(); // Exponential back-off
}
catch (HttpRequestException ex)
{
Console.WriteLine($"Network operation failed after {maxRetries} attempts: {ex.Message}");
throw; // Re-throw after max retries
}
}

In this example, the exception filter helps implement a retry mechanism that attempts the operation up to three times before giving up.

Advanced Patterns with Exception Filters

Combining Multiple Conditions

You can use logical operators to create more complex filtering conditions:

csharp
try
{
ProcessOrder(order);
}
catch (OrderException ex) when (ex.OrderId > 1000 && ex.ErrorCode == "INVENTORY_ERROR")
{
Console.WriteLine("High-priority order has inventory problems!");
NotifyManager(ex);
}
catch (OrderException ex) when (ex.ErrorCode == "PAYMENT_DECLINED")
{
Console.WriteLine("Customer payment was declined.");
NotifyCustomer(ex);
}
catch (OrderException ex)
{
Console.WriteLine($"Order processing error: {ex.Message}");
}

Exception Filters with External State

Exception filters can reference external variables and state:

csharp
public void ProcessUserData(UserData userData)
{
bool isVipCustomer = userData.CustomerLevel == CustomerLevel.VIP;

try
{
// Process user data
ValidateAndSaveUserData(userData);
}
catch (ValidationException ex) when (isVipCustomer)
{
// Special handling for VIP customers
Console.WriteLine("VIP customer data has validation issues.");
AutomaticallyFixAndRetry(userData, ex);
}
catch (ValidationException ex)
{
// Standard handling for regular customers
Console.WriteLine("Please correct the following issues with your data:");
foreach (var error in ex.ValidationErrors)
{
Console.WriteLine($"- {error}");
}
}
}

Performance Considerations

One important aspect of exception filters is that they are evaluated before the exception stack is unwound. This means:

  1. The performance impact is minimal compared to catching all exceptions and then checking conditions
  2. Stack trace information is preserved exactly as it was when the exception was thrown
  3. Exception filters don't trigger finally blocks while they're being evaluated

Here's an example showing the performance advantage:

csharp
// Less efficient approach (without exception filters)
try
{
SomeOperation();
}
catch (SomeException ex)
{
// Check condition after catching
if (ShouldHandle(ex))
{
HandleException(ex);
}
else
{
throw; // Re-throw if we shouldn't handle it
}
}

// More efficient approach (with exception filters)
try
{
SomeOperation();
}
catch (SomeException ex) when (ShouldHandle(ex))
{
HandleException(ex);
}

The second approach is more efficient because the runtime evaluates the filter condition before unwinding the stack, and if the condition is false, it immediately continues searching for another appropriate catch block.

Common Mistakes and Best Practices

Mistakes to Avoid

  1. Side effects in filter expressions: Although allowed, avoid code with side effects in the filter condition:
csharp
// Not recommended: side effect in filter
catch (Exception ex) when (Logger.LogError(ex) && ShouldHandle(ex))
  1. Overly complex filters: Keep filter conditions reasonably simple and focused:
csharp
// Too complex and hard to maintain
catch (Exception ex) when (ex is FileNotFoundException &&
ex.Message.Contains("config") &&
DateTime.Now.Hour < 12 &&
Environment.UserName == "admin")

Best Practices

  1. Use filters for their intended purpose - conditional exception handling, not for program flow control

  2. Create helper methods for complex filtering logic:

csharp
// Good practice - extract complex logic to a method
catch (DbException ex) when (IsCriticalDatabaseError(ex))
{
// Handle critical database errors
}

private bool IsCriticalDatabaseError(DbException ex)
{
// Complex logic to determine if this is a critical error
return ex.ErrorCode == -10 ||
(ex.Message.Contains("deadlock") && ex.InnerException != null);
}
  1. Combine with exception hierarchies for maximum flexibility:
csharp
catch (SqlException ex) when (ex.Number == 1205) // SQL deadlock
{
// Handle deadlock specifically
}
catch (SqlException ex) when (ex.Class > 20) // Fatal errors
{
// Handle fatal SQL errors
}
catch (SqlException ex) // All other SQL exceptions
{
// Handle other SQL errors
}
catch (DbException ex) // Other database exceptions
{
// Handle general database errors
}

Summary

C# exception filters provide a powerful way to conditionally catch exceptions based on custom logic. They allow you to:

  • Create more precise exception handling logic
  • Implement logging without affecting exception handling flow
  • Handle exceptions differently based on their specific properties
  • Create cleaner, more readable code compared to traditional exception handling patterns
  • Improve performance by avoiding unnecessary catch and re-throw operations

By mastering exception filters, you can write more robust, maintainable code that handles errors with greater precision and clarity.

Additional Resources

Exercises

  1. Write a program that reads a file but uses exception filters to handle different error scenarios (file not found, access denied, file in use) with specific messages.

  2. Create a web API method that uses exception filters to handle different types of database exceptions and returns appropriate HTTP status codes.

  3. Implement a retry mechanism using exception filters that only retries on specific network-related exceptions and gives up after a configurable number of attempts.

  4. Write a program that demonstrates how to use exception filters with logging to record errors without affecting the normal exception handling flow.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)