C# Multiple Catch Blocks: Syntax and Ordering
c# multiple catch blocks: Learn how to use multiple catch blocks in C# to handle different exception types, order them correctly, and apply exception filters for preci...
When a single try block can encounter several distinct failure modes, C# lets you attach multiple catch blocks to handle each exception type separately. This article covers the syntax, ordering rules, exception filters, and common pitfalls of c# multiple catch blocks so you can write error handling that behaves predictably in production.
Basic Syntax of Multiple Catch Blocks
The simplest form places one catch block after another, each specifying a different exception type. The runtime checks each catch block in the order they appear and executes the first one whose exception type matches the thrown exception.
try { var data = File.ReadAllText("config.json"); var settings = JsonSerializer.Deserialize<Settings>(data); } catch (FileNotFoundException ex) { Console.WriteLine($"Missing config file: {ex.FileName}"); } catch (JsonException ex) { Console.WriteLine($"Invalid JSON: {ex.Message}"); } catch (UnauthorizedAccessException ex) { Console.WriteLine($"No permission to read config: {ex.Message}"); }
Each catch block receives an exception variable that you can inspect. If you do not need the exception object, you can omit the variable name: catch (FileNotFoundException) still catches that type but does not bind a variable.
Order of Catch Blocks Matters
The compiler requires that more specific exception types appear before their base types. If you place catch (Exception) first, it will catch every exception, making later blocks unreachable. The compiler rejects this with an error, so you cannot accidentally write unreachable code in this way.
try { // ... } catch (Exception ex) { // Handles everything } catch (InvalidOperationException ex) // Compiler error: previous catch already handles this { // Unreachable }
This ordering rule exists because the runtime selects the first matching block. A derived exception type is a match for its base type, so the more specific block must come first. For example, FileNotFoundException derives from IOException, which derives from Exception. A correct order is FileNotFoundException, then IOException, then Exception.
Using Exception Filters with the when Keyword
C# 6 introduced exception filters, which let you add a condition to a catch block. The condition is evaluated only when an exception of that type is thrown. If the condition is false, the runtime continues to the next catch block, even if the type matches.
try { var result = await httpClient.GetAsync(url); } catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { Console.WriteLine("Resource not found."); } catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized) { Console.WriteLine("Authentication required."); } catch (HttpRequestException ex) { Console.WriteLine($"HTTP error: {ex.StatusCode}"); }
Filters are evaluated before the catch body runs, and they can call methods without altering the stack trace. This is particularly useful when you need to decide whether to handle an exception based on runtime state, such as a transient flag or a specific error code.
Catching Base and Derived Exceptions Together
You may need to handle a base exception type in one block and a derived type in another. The derived block must appear first. For instance, SqlException derives from DbException. If you want to handle SqlException with special logic and other database exceptions generically, write:
try { dbConnection.Open(); } catch (SqlException ex) when (ex.Number == -2) { // Timeout } catch (DbException ex) { // All other database errors }
This pattern keeps related error handling close together while preserving the ability to special-case a specific subtype.
Re-throwing Exceptions Without Losing Stack Trace
Inside a catch block, you often need to log the exception and then rethrow it so the caller can react. The correct way is throw; without an exception argument. This preserves the original stack trace. Using throw ex; resets the stack trace to the point of the rethrow, which hides where the exception originally occurred.
try { ProcessOrder(order); } catch (OrderValidationException ex) { _logger.LogError(ex, "Order validation failed"); throw; // preserves original stack trace }
If you need to wrap the exception in a new type, pass the original exception as the inner exception:
catch (OrderValidationException ex) { throw new OrderProcessingException("Failed to process order", ex); }
This preserves the original failure details for diagnostics while giving the caller a higher-level error type.
Common Mistakes and Misconceptions
One frequent mistake is assuming that a catch block with a filter that returns false will fall through to a later block of the same type. That is correct, but only if the later block appears after the filtered one. The runtime evaluates filters in order, so a later catch (HttpRequestException) will handle the exception if all earlier filters return false.
Another misconception is that a catch block without an exception type is the same as catch (Exception). In C#, catch { } is equivalent to catch (Exception) { } and catches all exceptions. It is rarely useful because you lose the exception object, but it can be used when you only need to clean up and rethrow.
Also, be careful with when filters that have side effects. The filter is evaluated before the catch body, and if it throws, that exception replaces the original one. Keep filters simple and avoid calling methods that can fail.
Performance and Maintainability Considerations
Exception filters can be more efficient than catching an exception and then checking a condition inside the catch block. When a filter returns false, the runtime continues searching for a matching catch block without unwinding the stack. This avoids the overhead of entering a catch block and then rethrowing. However, the difference is usually negligible unless exceptions are thrown frequently, which is already a performance problem.
From a maintainability perspective, multiple catch blocks make the error handling flow explicit. A reader can see exactly which exception types are handled and in what order. To keep the code readable, limit the number of catch blocks per try to a reasonable amount. If you find yourself writing more than four or five, consider grouping exceptions with filters or using a base exception type when the handling logic is identical.
When the handling logic is the same for several exception types, you can use a filter to combine them:
catch (Exception ex) when (ex is ArgumentException || ex is FormatException) { Console.WriteLine("Invalid input: " + ex.Message); }
This avoids duplicating the body across multiple catch blocks. However, be sure that these exception types do not have a base type that would make the filter redundant. If they share a common base, catching that base may be simpler.
Finally, remember that the order of catch blocks is also the order of evaluation. If you have a filter that is expensive to evaluate, place it after cheaper filters to avoid unnecessary work. In practice, keep filters simple and deterministic.
Using c# multiple catch blocks correctly is about matching the runtime's selection behavior to your error handling strategy. By ordering blocks from specific to general, using filters for conditional logic, and rethrowing with throw;, you can build error handling that is both robust and easy to maintain.