diff --git a/docs/csharp/language-reference/keywords/when.md b/docs/csharp/language-reference/keywords/when.md index b0aafe972e071..9c2ad43a385de 100644 --- a/docs/csharp/language-reference/keywords/when.md +++ b/docs/csharp/language-reference/keywords/when.md @@ -27,6 +27,8 @@ catch (ExceptionType [e]) when (expr) where *expr* is an expression that evaluates to a Boolean value. If it returns `true`, the exception handler executes; if `false`, it does not. +Exception filters with the `when` keyword provide several advantages over traditional exception handling approaches, including better debugging support and performance benefits. For a detailed explanation of how exception filters preserve the call stack and improve debugging, see [Exception filters vs. traditional exception handling](../statements/exception-handling-statements.md#exception-filters-vs-traditional-exception-handling). + The following example uses the `when` keyword to conditionally execute handlers for an depending on the text of the exception message. [!code-csharp[when-with-catch](~/samples/snippets/csharp/language-reference/keywords/when/catch.cs)] diff --git a/docs/csharp/language-reference/statements/exception-handling-statements.md b/docs/csharp/language-reference/statements/exception-handling-statements.md index ca4111d97edcd..cdcfe5f89e328 100644 --- a/docs/csharp/language-reference/statements/exception-handling-statements.md +++ b/docs/csharp/language-reference/statements/exception-handling-statements.md @@ -95,6 +95,43 @@ You can provide several `catch` clauses for the same exception type if they dist If a `catch` clause has an exception filter, it can specify the exception type that is the same as or less derived than an exception type of a `catch` clause that appears after it. For example, if an exception filter is present, a `catch (Exception e)` clause doesn't need to be the last clause. +##### Exception filters vs. traditional exception handling + +Exception filters provide significant advantages over traditional exception handling approaches. The key difference is **when** the exception handling logic is evaluated: + +- **Exception filters (`when`)**: The filter expression is evaluated *before* the stack is unwound. This means the original call stack and all local variables remain intact during filter evaluation. +- **Traditional `catch` blocks**: The catch block executes *after* the stack is unwound, potentially losing valuable debugging information. + +Here's a comparison showing the difference: + +:::code language="csharp" source="snippets/exception-handling-statements/WhenFilterExamples.cs" id="ExceptionFilterVsIfElse"::: + +## Advantages of exception filters + +- **Better debugging experience**: Since the stack isn't unwound until a filter matches, debuggers can show the original point of failure with all local variables intact. +- **Performance benefits**: If no filter matches, the exception continues propagating without the overhead of stack unwinding and restoration. +- **Cleaner code**: Multiple filters can handle different conditions of the same exception type without requiring nested if-else statements. +- **Logging and diagnostics**: You can examine and log exception details before deciding whether to handle the exception: + +:::code language="csharp" source="snippets/exception-handling-statements/WhenFilterExamples.cs" id="DebuggingAdvantageExample"::: + +### When to use exception filters + +Use exception filters when you need to: + +- Handle exceptions based on specific conditions or properties. +- Preserve the original call stack for debugging. +- Log or examine exceptions before deciding whether to handle them. +- Handle the same exception type differently based on context. + +:::code language="csharp" source="snippets/exception-handling-statements/WhenFilterExamples.cs" id="MultipleConditionsExample"::: + +### Stack trace preservation + +Exception filters preserve the original `ex.StackTrace` property. If a `catch` clause can't process the exception and re-throws, the original stack information is lost. The `when` filter doesn't unwind the stack, so if a `when` filter is `false`, the original stack trace isn't changed. + +The exception filter approach is valuable in applications where preserving debugging information is crucial for diagnosing issues. + #### Exceptions in async and iterator methods If an exception occurs in an [async function](../keywords/async.md), it propagates to the caller of the function when you [await](../operators/await.md) the result of the function, as the following example shows: diff --git a/docs/csharp/language-reference/statements/snippets/exception-handling-statements/WhenFilterExamples.cs b/docs/csharp/language-reference/statements/snippets/exception-handling-statements/WhenFilterExamples.cs new file mode 100644 index 0000000000000..9fa9f2419b166 --- /dev/null +++ b/docs/csharp/language-reference/statements/snippets/exception-handling-statements/WhenFilterExamples.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace ExceptionFilterExamples +{ + public static class WhenFilterExamples + { + // + public static void DemonstrateStackUnwindingDifference() + { + var localVariable = "Important debugging info"; + + try + { + ProcessWithExceptionFilter(localVariable); + } + catch (InvalidOperationException ex) when (ex.Message.Contains("filter")) + { + // Exception filter: Stack not unwound yet. + // localVariable is still accessible in debugger. + // Call stack shows original throwing location. + Console.WriteLine($"Caught with filter: {ex.Message}"); + Console.WriteLine($"Local variable accessible: {localVariable}"); + } + + try + { + ProcessWithTraditionalCatch(localVariable); + } + catch (InvalidOperationException ex) + { + // Traditional catch: Stack already unwound. + // Some debugging information may be lost. + if (ex.Message.Contains("traditional")) + { + Console.WriteLine($"Caught with if: {ex.Message}"); + Console.WriteLine($"Local variable accessible: {localVariable}"); + } + else + { + throw; // Re-throws and further modifies stack trace. + } + } + } + + private static void ProcessWithExceptionFilter(string context) + { + throw new InvalidOperationException($"Exception for filter demo: {context}"); + } + + private static void ProcessWithTraditionalCatch(string context) + { + throw new InvalidOperationException($"Exception for traditional demo: {context}"); + } + // + + // + public static void HandleFileOperations(string filePath) + { + try + { + // Simulate file operation that might fail. + ProcessFile(filePath); + } + catch (IOException ex) when (ex.Message.Contains("access denied")) + { + Console.WriteLine("File access denied. Check permissions."); + } + catch (IOException ex) when (ex.Message.Contains("not found")) + { + Console.WriteLine("File not found. Verify the path."); + } + catch (IOException ex) when (IsNetworkPath(filePath)) + { + Console.WriteLine($"Network file operation failed: {ex.Message}"); + } + catch (IOException) + { + Console.WriteLine("Other I/O error occurred."); + } + } + + private static void ProcessFile(string filePath) + { + // Simulate different types of file exceptions. + if (filePath.Contains("denied")) + throw new IOException("File access denied"); + if (filePath.Contains("missing")) + throw new IOException("File not found"); + if (IsNetworkPath(filePath)) + throw new IOException("Network timeout occurred"); + } + + private static bool IsNetworkPath(string path) + { + return path.StartsWith(@"\\") || path.StartsWith("http"); + } + // + + // + public static void DemonstrateDebuggingAdvantage() + { + var contextData = new Dictionary + { + ["RequestId"] = Guid.NewGuid(), + ["UserId"] = "user123", + ["Timestamp"] = DateTime.Now + }; + + try + { + // Simulate a deep call stack. + Level1Method(contextData); + } + catch (Exception ex) when (LogAndFilter(ex, contextData)) + { + // This catch block may never execute if LogAndFilter returns false. + // But LogAndFilter can examine the exception while the stack is intact. + Console.WriteLine("Exception handled after logging"); + } + } + + private static void Level1Method(Dictionary context) + { + Level2Method(context); + } + + private static void Level2Method(Dictionary context) + { + Level3Method(context); + } + + private static void Level3Method(Dictionary context) + { + throw new InvalidOperationException("Error in deep call stack"); + } + + private static bool LogAndFilter(Exception ex, Dictionary context) + { + // This method runs before stack unwinding. + // Full call stack and local variables are still available. + Console.WriteLine($"Exception occurred: {ex.Message}"); + Console.WriteLine($"Request ID: {context["RequestId"]}"); + Console.WriteLine($"Full stack trace preserved: {ex.StackTrace}"); + + // Return true to handle the exception, false to continue search. + return ex.Message.Contains("deep call stack"); + } + // + } +} \ No newline at end of file