C#
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C#
-
Section 4: Working with Data
-
Section 5: Error Handling
-
Section 6: Delegates and Events
-
Section 7: Async Programming
-
Section 8: More Language Features
-
Section 9: File I/O and Serialization
-
Section 10: Networking in .NET
-
Section 11: The .NET Ecosystem
-
Section 12: Memory and Performance
-
Section 13: Concurrency Beyond Async
-
Section 14: Reflection and Attributes
-
Section 15: Testing and Best Practices
-
Section 16: Design Patterns in C#
-
Section 17: Standard Library Deep Dive
-
Section 18: Data Structures and Algorithms in C#
-
Section 19: GUI and Desktop Development Overview
-
Section 20: Practical Projects
-
Section 21: More Practice Exercises
-
Section 22: More Standard Library and Text Processing
-
Section 23: More Design Patterns
-
Section 24: More Projects
-
Section 25: Interview Practice
-
Section 26: C# Keywords Reference (Modifiers)
-
Section 27: C# Keywords Reference (Statements)
-
Section 28: C# Keywords Reference (Operators)
-
Section 29: BCL: System.Collections.Generic
-
Section 30: BCL: System.Linq
-
Section 31: BCL: System.Threading
-
Section 32: BCL: System.IO
-
Section 33: BCL: System.Text and System.Text.Json
-
Section 34: BCL: System.Net.Http
-
Section 35: C# Language Specification Topics
-
Section 36: More Practice Exercises
-
Section 37: More Async Patterns
-
Section 38: More BCL: System.Reflection and System.Diagnostics
-
Section 39: Nullable Reference Types In Depth
-
Section 40: C# Records and Pattern Matching In Depth
-
Section 41: Dependency Injection Deep Dive
-
Section 42: More Interview and Whiteboard Practice
43: Exception Filters
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
PaymentExceptionclass that has a string property calledErrorCode. - Write a method
ProcessPayment()that randomly throws one of the four error codes mentioned above. - Implement a
try-catchblock using an exception filter (thewhenkeyword) 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).
There are no comments for now.