AlgoMaster Logo

Multiple Catch Blocks

Last Updated: May 17, 2026

12 min read

A single try can be paired with as many catch blocks as you want, and each one handles a different exception type. That's the mechanism C# uses to react differently to different failures: a missing product, a bad coupon code, a network blip, a programming bug. This lesson covers how the runtime picks one catch out of the list, why the order you write them in is load-bearing, and the compiler error you get if you write them in the wrong order.

Why Multiple Catches

Real code can fail in more than one way. A checkout method might pull a product from a dictionary (KeyNotFoundException), parse a quantity from user input (FormatException), apply a discount that requires a non-null coupon (ArgumentNullException), and write the order to disk (IOException). If a single broad catch handles every type the same way, the user has no idea whether the product is missing, the quantity is wrong, or something else broke. Multiple catches let you respond to each failure on its own terms:

Each catch handles one kind of failure with a useful message. The shape of the code is the same as the single-catch version, with one extra catch block per exception type you want to handle distinctly. Logging a missing product is different from retrying a network call, which is different from rolling back a transaction. Multiple catches give each kind of failure its own response, while keeping the happy path inside the try block uncluttered. A single try can have many catches if many distinct outcomes need distinct handling; in practice, two to four is the common range.

How the Runtime Picks One

When a throw fires inside a try, the runtime decides which catch handles it. The rule is simple: it walks the catches in source order, top to bottom, and runs the first one whose declared type matches the thrown exception. A match means the thrown exception's type is the same as the declared type or derived from it.

Each input throws a different exception type, and each one lands in a different handler. The first call throws ArgumentNullException, which matches the first catch. The second throws plain ArgumentException, which is the parent of ArgumentNullException (not a child), so it doesn't match the first catch and ends up in the second. The third throws FormatException, which isn't an ArgumentException either, so the runtime keeps walking and lands in the third handler.

The matching test is "is the thrown exception assignable to the declared catch type?", the same is check the language uses everywhere. ArgumentNullException is an ArgumentException (because it derives from it), so a thrown ArgumentNullException would match a catch (ArgumentException) if no more specific catch came first. The reverse isn't true: a thrown ArgumentException is not an ArgumentNullException.

For each catch block in source order, the runtime asks "does this catch's declared type match the thrown exception?" The first yes wins. If every check returns no, the exception keeps propagating up the call stack to look for a handler in the caller. The _try-catch Block_ lesson covered that propagation; here the focus is which catch is chosen.

The search runs at runtime, not at compile time. The compiler knows the static types of the catch blocks, but it doesn't know which one will fire on any given call. The decision is made fresh each time an exception is thrown, based on the actual runtime type of the thrown object.

The exception object is fully constructed before any catch is consulted, including its message and stack trace. That part takes real CPU time. The walk through the catch list is the cheap part.

Order Matters: Most Specific First

Because the runtime walks the catches in source order and runs the first match, ordering them most specific to most general is the only ordering that lets every catch do its job. A catch for a parent type is broader than a catch for a child type. If the parent catch comes first, the child catch is unreachable.

The exception hierarchy here is the one the _Exception Basics_ lesson introduced. Exception is the root. SystemException derives from it. ArgumentException derives from SystemException. ArgumentNullException and ArgumentOutOfRangeException derive from ArgumentException:

Every child is also a kind of its parent. An ArgumentNullException is an ArgumentException, which is a SystemException, which is an Exception. So a catch (Exception) would handle an ArgumentNullException, and a catch (ArgumentException) would too. The most specific catch that names ArgumentNullException gives the most precise response, so it should come first. The right order, from specific to general:

The first throw is ArgumentNullException: exact match on the first catch, run it. The second throw is plain ArgumentException: not an ArgumentNullException, not an ArgumentOutOfRangeException, matches catch (ArgumentException). The third throw is ArgumentOutOfRangeException: not an ArgumentNullException, matches catch (ArgumentOutOfRangeException) exactly. The catch for plain ArgumentException was passed over because the more specific one matched first.

The ordering rule is about which catch the runtime can reach. Putting the parent type first doesn't break the program for a child exception; the parent catch would handle it. The problem is that the child catch then never runs, no matter what's thrown, so it's dead code. The compiler refuses to compile this. Concretely, "parent first" looks like this:

The second catch is unreachable. Every ArgumentException is also an Exception, so the first catch claims it. The compiler flags this. The fix is to swap the order:

Both catches are reachable. An ArgumentException lands in the first; anything else (like an IOException) falls through to the second.

The rule applies recursively. If you have catches for ArgumentNullException, ArgumentException, and Exception, the order has to be exactly that. For unrelated types (FormatException and OverflowException, neither derives from the other), the order between them doesn't matter, because neither one's match set overlaps with the other's.

The CS0160 Compiler Error

When you write a catch for a general type before a catch for a more specific type, the compiler refuses to build the program. The exact error is:

"A previous catch clause" means a catch that appears earlier in the source. "Already catches all exceptions of this or of a super type" means the previous catch's declared type is the same as, or an ancestor of, the current catch's declared type. The parenthetical tells you which previous type is the culprit. A minimal example that triggers it:

Compiler output:

The first catch declares Exception, which is FormatException's ancestor. Any FormatException thrown inside the try would match the first catch first, so the second catch can never execute. Rather than silently accept dead code, the compiler stops you from writing it. The fix is the swap we saw earlier:

FormatException matches the first catch. The second catch sits there to handle anything else that might leak out of the try, but isn't consulted because the more specific one fired first.

The error appears for any pair where the earlier catch's type is an ancestor of the later catch's type. That includes the direct parent, grandparent, great-grandparent, and so on:

The fix in every case is the same: swap them, or remove the redundant one if you really did want only the general handler. The error message names the offending type in parentheses, which is the type the earlier catch declared. When you have three or four catches and you're not sure which two are colliding, that parenthetical is the diagnostic.

The Catch-All Pattern

A final catch (Exception) (or catch (Exception ex) when you want the object) acts as a safety net: it handles anything the more specific catches didn't claim. The pattern is common at the boundaries of a program, like the outermost layer of a web request, a background job, or the top of an event handler, where any uncaught exception would terminate the process or leak a stack trace to a user.

Output (when the log write succeeds):

The three known failure modes each have a specific handler with a useful message. The catch-all at the bottom catches anything else, like an OutOfMemoryException or a third-party library throwing something unexpected, and prevents the program from crashing without diagnostics. Using ex.GetType().Name is a useful trick: it tells you what type slipped through, which is the first thing you want to know when triaging.

The trade-off is that a catch-all can hide bugs. A NullReferenceException or an IndexOutOfRangeException is almost always a code bug, not a user error. The common defense is to log the full exception inside the catch-all so information is preserved even when the user only sees a generic message, and to rethrow with throw; in deeper layers so the failure keeps propagating. The _throw & Rethrow_ lesson covers rethrow mechanics.

The Bare catch Form

C# allows a catch block with no type at all: just catch { ... }. This is the bare catch form, and it catches absolutely anything that crosses the try boundary, including some objects that don't derive from System.Exception.

That program runs without surprises. The interesting part isn't what the example shows; it's what the bare catch can catch that a catch (Exception) cannot. To understand that, a bit of history helps.

C# is one language on the CLR (Common Language Runtime). Other CLR languages (notably some older C++/CLI code) could throw objects that don't derive from System.Exception. The base class library calls these "non-CLS-compliant exceptions." Bare catch catches both kinds. A catch (Exception) in modern .NET also catches them, because the runtime wraps non-CLS-compliant throws in RuntimeWrappedException before delivering them. So on .NET 8, the two forms catch the same set in practice.

The stylistic difference is that catch (Exception ex) gives you the exception object; bare catch doesn't. If you want to log the message, inspect the type, or rethrow with throw;, you want the typed form. Bare catch is a one-line "swallow everything" that throws away the exception entirely.

The compiler enforces the same ordering rules on bare catch as on typed catches. A bare catch acts as if it caught the universal type, so anything after it is unreachable:

The error code for "anything after bare catch" is CS1017, a slightly different one from CS0160, but the cause and the fix are the same: move the bare catch to the bottom or remove the later catches. In modern C# code, prefer catch (Exception ex) so you have the exception object to work with, and reserve bare catch for the rare case where it really is the right answer.

What Happens After a Match

Once the runtime picks a catch and runs its body, the exception is considered handled. Other catches in the same try/catch block are not consulted, no matter how related their types are. The flow continues after the entire try/catch group, just as if the try had completed normally.

The thrown ArgumentNullException matches the first catch. That handler runs, and execution moves on to the statement after the try/catch group. The second and third catches don't run, even though their types would also match the thrown object if asked. They aren't asked. The first match ends the search.

A try/catch runs only one catch, period. There is no fall-through, no syntax to ask the next catch to also run. If you need two responses to the same exception, both go inside the one catch block.

Both lines run because they live inside the same catch block. Three responses to the same exception become three statements inside one handler, not three catches for the same type. The compiler wouldn't accept three catches for the same type anyway; CS0160 would fire because each would be a duplicate of the one before.

The single-match rule applies even when an exception inside one catch body throws something a later catch could have handled. Once you're in a catch body, you're no longer inside the original try, so the original try's sibling catches don't see what you throw from here. An exception thrown from inside a catch propagates to the next enclosing try/catch, not to the catches that sat alongside the current one.

Catch blocks under a single try are a list of "first-match-wins" alternatives, not a chain that combines. Each one is a complete response; whichever one fires owns the recovery. We'll see in the next two lessons how finally runs regardless of which catch fired and how throw; from inside a catch resumes propagation to the next enclosing handler.

A Quick Note on when Filters

Multiple catches plus the hierarchy give a lot of expressiveness, but they pick a handler based only on the thrown exception's type. Sometimes you want to pick based on a runtime condition too. C# 6 introduced exception filters with the when keyword for exactly this. You add a boolean condition after the catch's type, and the catch runs only when both the type matches and the condition is true:

Both catches declare InvalidOperationException, but the first one has a when filter that only matches messages containing "cancelled." The runtime checks the type; if it matches, it evaluates the filter; if the filter is false, the catch is skipped and the walk continues. Two catches for the same type are legal as long as at least the first one has a filter.

The full mechanics of when (when the filter runs, what state it can read, performance) are the subject of the _Exception Filters (when clause)_ lesson. For now, the takeaway is that multiple catches and when filters compose: the type-based selection from this chapter is the foundation, and when is a refinement on top.

Looking Ahead

Multiple catches are how C# lets you respond to different failures with different recovery code in a single try block. The runtime picks the first catch whose declared type matches the thrown exception, the compiler enforces "most specific first" with CS0160, and only one catch runs per throw. The _finally Block_ lesson covers finally, the block that runs regardless of which catch matched (or whether any catch matched at all), and which is where cleanup code like closing files or releasing locks usually lives.

Summary

  • A try can be followed by multiple catch blocks, each handling a different exception type, so different failures can produce different responses.
  • The runtime walks the catches in source order and runs the first one whose declared type matches the thrown exception. Only that catch runs; subsequent catches in the same group are not consulted.
  • Order catches from most specific to most general. A child type comes before its parent. Exception (the root) comes last when used as a catch-all.
  • The compiler enforces ordering with CS0160: A previous catch clause already catches all exceptions of this or of a super type. The fix is to swap the offending pair so the specific catch precedes the general one.
  • A final catch (Exception ex) at outer layers acts as a safety net and is common in request handlers, background jobs, and event loops. Pair it with logging or rethrow so it doesn't hide bugs.
  • Bare catch { } catches everything but discards the exception object. In modern .NET it has no real advantage over catch (Exception ex), which is the better default.
  • The cost of evaluating multiple catches is paid only on the unhappy path. The walk stops at the first match. Don't avoid adding a catch out of performance fear; the happy path doesn't see it.
  • when filters (the _Exception Filters (when clause)_ lesson) refine catch selection by adding a boolean condition, which makes two catches for the same exception type legal when at least one is filtered.

The _finally Block_ lesson covers how to run cleanup code that fires regardless of whether the try completed normally, which catch handled an exception, or whether the exception escaped uncaught.