Skip to Content
Course content
Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've spent a lot of time reviewing pull requests over the years, and there is one pattern that always makes me itch: the "Catch-Check-Throw" sequence. It usually looks like this: you catch a generic exception, use an if statement to see if it's the specific error you care about, and if it isn't, you throw; it back up the chain. It feels intuitive, but it's actually a subtle bug in your debugging strategy.

"Catching and Re-throwing is Close Enough"

The misconception here is that catch (Exception ex) { if (condition) { ... } else { throw; } } is functionally identical to an exception filter. On the surface, the logic flow is the same: if the condition isn't met, the exception keeps moving up the call stack. But here is why that's wrong: the moment that catch block executes, the runtime considers the exception "handled" for a brief window, and the stack begins to unwind.

If you're debugging a complex crash in production using a memory dump, this is a nightmare. When you re-throw, you've modified the state of the stack. You lose the original context of where the exception was first thrown because you've essentially "stopped" the exception and started it again. I can't tell you how many hours I've wasted chasing bugs because someone "captured" the exception just to check a property before throwing it again.

Keeping the Stack Trace Intact with when

This is where Exception Filters come in. By using the when keyword, you tell the .NET runtime: "Don't actually catch this exception unless this boolean condition is true." If the condition is false, the runtime keeps looking for a handler without ever leaving the original stack frame.

Let's look at a real-world scenario. Imagine you're writing a client for a cloud API. You only want to handle HttpRequestException if the status code is 429 (Too Many Requests) so you can implement a retry logic. For any other HTTP error, you want the global error handler to deal with it.

try 
{
    var response = await _apiClient.GetAsync("/data");
} 
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.TooManyRequests) 
{
    // This block ONLY executes if the status is 429.
    // The stack remains pristine until this point.
    await HandleRateLimitAsync();
}
// If it's a 404 or 500, the runtime acts as if the catch block above doesn't exist.

Notice the difference? The when clause is evaluated before the exception is caught. If ex.StatusCode is HttpStatusCode.NotFound, the runtime doesn't enter the catch block at all; it just keeps searching for the next compatible handler. To the debugger, it looks as if the exception flew straight through that block, preserving the exact state of the application at the moment of the crash.

A few things to keep in mind as you use these:

  • Filters can call methods. You can move complex logic into a helper method like when (IsTransientError(ex)) to keep your catch blocks clean.
  • Be careful not to put "side-effect" logic inside a filter (like logging to a database). Filters can be executed multiple times by the runtime as it searches for a handler. Keep them pure and read-only.
  • You can stack multiple catch blocks with different filters for the same exception type, and they will be evaluated in order.



📋 Practical Task

Implementing a Selective Retry Filter for a Mock Payment Gateway

You are integrating with a third-party payment processor. The processor throws a PaymentException. However, you should only catch and handle the exception if the ErrorCode is "INSUFFICIENT_FUNDS" or "CARD_EXPIRED". If the error is "SYSTEM_FAILURE" or "FRAUD_ALERT", you must let the exception propagate up to the global logger so the security team is notified.

Requirements:

  • Create a PaymentException class that has a string property called ErrorCode.
  • Write a method ProcessPayment() that randomly throws one of the four error codes mentioned above.
  • Implement a try-catch block using an exception filter (the when keyword) to catch only the "user-fixable" errors (Insufficient Funds or Expired Card).
  • Inside the catch block, print a user-friendly message.
  • Ensure that "SYSTEM_FAILURE" and "FRAUD_ALERT" are not caught by your filter and are allowed to crash the program (or be caught by a top-level handler).
Rating
0 0

There are no comments for now.

to be the first to leave a comment.