Using C# Exception Filters to Control Error Handling
c# exception filter: Learn how C# exception filters work, when to use them, and how they differ from traditional catch blocks.
C# exception filters, introduced in C# 6, let you attach a condition to a catch block using the when keyword. Instead of unconditionally handling an exception, the filter decides whether the catch block should run. This small syntactic addition changes how you structure error handling and can simplify code that would otherwise require nested if statements or re-throwing exceptions.
What Is an Exception Filter?
An exception filter is a boolean expression that appears after when in a catch clause. The catch block executes only if the expression evaluates to true. If it evaluates to false, the runtime continues searching for another matching catch block, and if none exists, the exception propagates up the call stack.
try { // Some operation that may throw } catch (InvalidOperationException ex) when (ex.Message.Contains("critical")) { // Only handles InvalidOperationException with "critical" in the message }
The filter expression can reference the exception variable declared in the catch clause. It can also reference variables from the enclosing method, though that is rarely necessary and can create subtle dependencies.
How Exception Filters Differ from Plain Catch Blocks
A traditional catch block always runs when the exception type matches. If you need conditional handling, you typically put an if inside the catch body:
try { // Operation } catch (InvalidOperationException ex) { if (ex.Message.Contains("critical")) { // Handle } else { throw; // Re-throw to preserve stack trace } }
The filter version is more direct:
try { // Operation } catch (InvalidOperationException ex) when (ex.Message.Contains("critical")) { // Handle }
When the condition is false, the filter version does not enter the catch block at all. This means the exception is not caught and re-thrown; instead, the runtime treats the catch block as if it did not match. The stack trace remains intact, and any outer catch blocks still get a chance to handle the exception.
Using Filters with Multiple Catch Blocks
Exception filters become especially useful when you have several catch blocks that could handle the same exception type under different conditions. The runtime evaluates filters in order, and the first catch block whose filter returns true handles the exception.
try { // Network request } catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { // Handle 404 specifically } catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized) { // Handle 401 specifically } catch (HttpRequestException ex) { // Handle other HTTP errors }
Without filters, you would need a single catch block with a series of if/else checks, which is harder to read and more prone to mistakes when re-throwing.
Exception Filters and Stack Unwinding
One of the less obvious benefits of exception filters is their effect on stack unwinding. When an exception is thrown, the runtime walks the stack looking for a matching handler. For a traditional catch block, the stack is unwound as soon as the type matches, even if the code inside the catch later re-throws. This unwinding loses the original stack trace unless you use throw; carefully.
With a filter, the runtime evaluates the condition before unwinding the stack. If the filter returns false, the runtime continues searching without unwinding. This behavior preserves the original exception context and can be more efficient when you have many catch blocks that don't match.
Performance differences are usually negligible in typical applications, but they can matter in high-throughput error paths where exceptions are thrown frequently. The key point is that filters avoid the cost of entering and exiting a catch block when the condition is false.
Practical Use Case: Logging Without Catching
A common pattern is to log an exception but let it propagate. You can do this with a filter that always returns false after logging:
try { // Operation } catch (Exception ex) when (LogAndReturnFalse(ex)) { // Never reached } static bool LogAndReturnFalse(Exception ex) { // Log the exception Console.WriteLine($"Logging: {ex.Message}"); return false; }
Because the filter returns false, the catch block is skipped, and the exception continues to propagate. This is cleaner than catching, logging, and re-throwing, and it preserves the original stack trace without any risk of accidentally swallowing the exception.
Pitfalls and Limitations
Exception filters are not a free pass. The filter expression runs on every exception that matches the type, so it should be fast and free of side effects that affect program state. If the filter itself throws, that new exception replaces the original one, which can mask the root cause.
Filters also behave differently with async methods. You cannot use await inside a filter because filters are synchronous. If you need to perform asynchronous logging, you must block or use a synchronous logging method instead.
Another limitation is that filters are evaluated in the context of the throw, not the catch. This means local variables from the try block are not available in the filter, only the exception object and outer scope variables. This is usually fine, but it can surprise developers who expect to inspect local state.
Maintainability and When to Use Filters
Use exception filters when the decision to handle an exception depends on properties of the exception itself, such as status codes, error codes, or message content. They are also useful for cross-cutting concerns like logging or telemetry that should not alter control flow.
Avoid filters when the condition is complex or involves method calls that could have side effects. In such cases, a plain catch block with an if may be clearer. Filters are a language feature, not a mandate; the goal is to make error handling more readable and less error-prone, not to use the newest syntax everywhere.
A final consideration: because filters are evaluated before the catch body, they run even if the catch block would later re-throw. This can be an advantage for logging, but it also means the filter runs even when the exception is ultimately not handled by that catch. Make sure the filter does not assume it will be the final handler.