Advanced Error Handling in C#: From Basic try/catch to Industrial Patterns
\n\nError handling is not just about catching exceptions; it's the art of designing reliable systems. In C# (.NET), developers often limit themselves to the try/catch construct, but the platform offers much more powerful mechanisms. In this article, we will explore advanced techniques: from exception filters to retry patterns and error context propagation.
1. Exception Filters: Precision Instead of Stubs
\n\nException Filters, introduced in C# 6.0, allow you to specify a condition for entering a catch block without re-throwing the exception. This is significantly cleaner than the classic approach with if inside the block.
try\n{\n // Code that may throw an exception\n var result = ProcessData(input);\n}\ncatch (CustomException ex) when (ex.ErrorCode == 1001)\n{\n // Handle only errors with code 1001\n Log.Error($"Critical error: {ex.Message}");\n throw; // Rethrow if needed\n}\ncatch (CustomException ex) when (ex.IsRecoverable)\n{\n // Recoverable error — log and continue\n Log.Warning($"Recoverable error: {ex.Message}");\n RetryOperation();\n}\n\nAdvantages:
\n- \n
- No need for a second
throwfor filtering. \n - Improves readability and performance (call stack is not corrupted). \n
- Multiple
catchblocks with different conditions can be combined. \n
2. Retry Pattern and Polly
\n\nNot all errors are fatal. For transient failures (network issues, database locks), use the retry pattern. Built-in C# capabilities are limited, so the Polly library is recommended.
\n\nusing Polly;\nusing Polly.Retry;\n\n// Define the retry policy\nAsyncRetryPolicy retryPolicy = Policy\n .Handle<HttpRequestException>() // Handle only HTTP errors\n .WaitAndRetryAsync(\n 3, // Number of retries\n retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), // Exponential backoff\n onRetry: (exception, timeSpan, retryCount, context) =>\n {\n Log.Warning($"Attempt {retryCount} failed. Retrying in {timeSpan.Seconds} sec.");\n });\n\n// Apply the policy\nawait retryPolicy.ExecuteAsync(async () =>\n{\n var response = await httpClient.GetAsync("https://api.example.com/data");\n response.EnsureSuccessStatusCode();\n});\n\nImportant: Do not use infinite retries. Always set a maximum number of attempts and use a Circuit Breaker to protect the system from cascading failures.
\n\n3. Proper Exception Propagation: throw vs throw ex
\n\nOne of the most common mistakes is losing the call stack. The difference between throw and throw ex is critical for debugging.
try\n{\n // ...\n}\ncatch (Exception ex)\n{\n // WRONG: loses the original stack\n // throw ex; \n\n // CORRECT: preserves the stack\n throw;\n\n // OR: adds context without losing the original\n // throw new ApplicationException("Error in module X", ex);\n}\n\nIf you need to add context, use ExceptionDispatchInfo (from the System.Runtime.ExceptionServices namespace) to precisely preserve the stack when rethrowing across threads.
ExceptionDispatchInfo.Capture(exception).Throw(); // Preserves the original stack\n\n4. Custom Exceptions and Error Context
\n\nStandard exceptions often lack business context. Create your own exception classes with additional properties.
\n\npublic class PaymentProcessingException : Exception\n{\n public int OrderId { get; }\n public dec