-
Notifications
You must be signed in to change notification settings - Fork 6k
Add explanation of why the when
contextual keyword is better than if/else in catch blocks
#47887
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
e057611
465c5e2
b2f3e32
bfbf5d9
4b6db00
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 stack is unwound *before* entering the catch block, 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:** | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know Copilot likes to add bolding using |
||
|
||
1. **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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This shouldn't be a numbered list since it's not a sequential procedure. |
||
1. **Performance benefits**: If no filter matches, the exception continues propagating without the overhead of stack unwinding and restoration. | ||
1. **Cleaner code**: Multiple filters can handle different conditions of the same exception type without requiring nested if-else statements. | ||
1. **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 | ||
Comment on lines
+122
to
+125
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add periods. |
||
|
||
:::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: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Code comments should end in punctuation. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.IO; | ||
|
||
namespace ExceptionFilterExamples | ||
{ | ||
public static class WhenFilterExamples | ||
{ | ||
// <ExceptionFilterVsIfElse> | ||
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}"); | ||
BillWagner marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
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}"); | ||
} | ||
// </ExceptionFilterVsIfElse> | ||
|
||
// <MultipleConditionsExample> | ||
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"); | ||
} | ||
// </MultipleConditionsExample> | ||
|
||
// <DebuggingAdvantageExample> | ||
public static void DemonstrateDebuggingAdvantage() | ||
{ | ||
var contextData = new Dictionary<string, object> | ||
{ | ||
["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<string, object> context) | ||
{ | ||
Level2Method(context); | ||
} | ||
|
||
private static void Level2Method(Dictionary<string, object> context) | ||
{ | ||
Level3Method(context); | ||
} | ||
|
||
private static void Level3Method(Dictionary<string, object> context) | ||
{ | ||
throw new InvalidOperationException("Error in deep call stack"); | ||
} | ||
|
||
private static bool LogAndFilter(Exception ex, Dictionary<string, object> 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"); | ||
} | ||
// </DebuggingAdvantageExample> | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Before" is italicized in both bullet points to add emphasis, which doesn't make much sense since they're trying to point out differences.