Skip to content

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

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/csharp/language-reference/keywords/when.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <xref:System.Net.Http.HttpRequestException> depending on the text of the exception message.

[!code-csharp[when-with-catch](~/samples/snippets/csharp/language-reference/keywords/when/catch.cs)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Comment on lines +102 to +103
Copy link
Contributor

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.


Here's a comparison showing the difference:

:::code language="csharp" source="snippets/exception-handling-statements/WhenFilterExamples.cs" id="ExceptionFilterVsIfElse":::

**Advantages of exception filters:**
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know Copilot likes to add bolding using **, but it actually goes against style guidelines.


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.
Copy link
Contributor

Choose a reason for hiding this comment

The 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
Copy link
Contributor

Choose a reason for hiding this comment

The 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:
Expand Down
Copy link
Contributor

Choose a reason for hiding this comment

The 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}");
}
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>
}
}