C# example

How to catch multiple exceptions in C#

8 min read Updated Oct 2026 Runs in an isolated runtime
Quick answer

Use one catch with an exception filter: catch (Exception e) when (e is FormatException or OverflowException). The block runs for either type, and every other exception passes straight through as if the catch weren't there. To rethrow from inside a catch, write throw;, not throw e;, which throws away the original stack trace.

C# has no catch (A | B e) syntax like Java. Before C# 6 the choices were a copy of the handler per type, or catching Exception and rethrowing what you didn't want. Exception filters (C# 6) and or patterns (C# 9) fixed that, and a filter can test anything about the exception, not just its type. The other half of the question is what to do once you're in the catch: rethrow it, wrap it with more context, or keep it to throw later, without losing where it came from. Each example runs on this page: hit Run, then edit the code and run it again.

1One catch with a when filterRecommended

Catch a common base type, usually Exception, and add when (e is FormatException or OverflowException). The runtime checks the filter before it picks the handler. When the filter is false, the catch doesn't catch at all, and the exception carries on to the next handler up the stack. That makes it different from catching Exception and testing the type inside the block: nothing you didn't list is ever caught, so nothing needs rethrowing.

Program.cs

Output

Prints 42 -> 42, then abc -> FormatException: The input string 'abc' was not in a correct format. and 99999999999 -> OverflowException: Value was either too large or too small for an Int32. from the same block, then 7 -> 7. int.Parse(null) throws ArgumentNullException, which the filter doesn't list, so the inner catch is skipped and the outer one prints outer handler: ArgumentNullException. On C# 6 to 8, write the filter as when (e is FormatException || e is OverflowException); it works the same. For parsing in particular, int.TryParse avoids the exceptions altogether, see converting a string to an int.

2Separate catch blocks, and their order

When each type needs different handling, give each its own catch. The runtime tries them top to bottom and runs the first one whose type matches, so a base class has to come after the classes that derive from it. When the bodies would be the same, move the body into a method rather than copying it. The last part shows the pattern you still find in older code: catch the base type, test it, and throw; whatever you didn't mean to catch.

Program.cs

Output

Prints abc -> FormatException and 99999999999 -> OverflowException from the two specific blocks. "12" parses, then 100 / 0 throws DivideByZeroException, which no block names; it derives from ArithmeticException, so the third block prints 12 -> other arithmetic error: DivideByZeroException. Put catch (ArithmeticException) above catch (OverflowException) and the program no longer compiles: error CS0160, "A previous catch clause already catches all exceptions of this or of a super type". The legacy block prints legacy pattern caught FormatException. It works, but by the time it rethrows the stack has already unwound to this method, which a filter avoids.

3Filter on more than the type

A filter is any bool expression, so it can look at the exception's properties: a status code, an error number, a message. That lets one exception type go to different handlers, or only some of its instances get caught. Filters also run before the stack unwinds, while the method that threw is still on the stack and its finally blocks haven't run. A filter that logs and returns false records every exception and catches none of them.

Program.cs

Output

The first loop prints 503: retrying later and 429: retrying later, and 404: not retryable, passed up to the caller, because the filter was false and the outer handler got it. In the second part, the order of the lines is the point: log: IOException: disk full comes before Save's finally block, so the filter ran while Save was still on the stack. Only then does the exception unwind and reach handled further up: disk full. ApiException uses a primary constructor, which classes got in C# 12; on older versions, write a normal constructor.

4throw vs throw ex

After logging or cleaning up, you often want the exception to carry on. throw; on its own rethrows the exception being handled with its stack trace as it was. throw ex; throws the same object, but as if it were thrown fresh from that line, so the trace now starts in the catch and the frames that show where it really failed are gone.

Program.cs

Output

With throw; the trace still runs from System.Int32.Parse(String s) through Orders.ParseQuantity(String text) and Orders.LoadWithThrow() to Program.<Main>$(String[] args) (the first frame is the runtime helper that throws it). With throw ex; it is two lines, Orders.LoadWithThrowEx() and Program.<Main>$, and nothing says the problem was in ParseQuantity. The .NET code analysis rule CA2200 flags throw ex; for this reason. If you don't need the variable, catch (FormatException) without a name makes throw ex; impossible to write. On a build with its .pdb symbols, each frame also ends with a file name and line number.

5Wrap with context, or rethrow later

The good reasons to catch and rethrow are to add something the exception doesn't know, or to throw it at a better moment. To add context, throw a new exception and pass the caught one as the innerException argument, so the original type, message and trace stay attached. To finish a loop first and then report the failure, keep the exception in an ExceptionDispatchInfo and call Throw() later. That rethrows the same exception with its original trace, which throw e; outside the catch would lose.

Program.cs

Output

The wrapped exception prints Line 3: 'ten' is not a quantity, which says which line and which value, and its InnerException is still the FormatException with its own message. The second part adds up the good lines, parsed total: 10, then throws the captured error. Its trace starts in System.Int32.Parse and Importer.ParseQuantity, where it was thrown, followed by --- End of stack trace from previous location --- and the frame where Throw() ran. A catch that only does throw; and nothing else adds nothing: delete it.

6Which should you use?

PatternWhat it doesStack traceBest for
catch (Exception e) when (e is A or B)One handler for the listed typesUntouched for everything elseSame handling for several types
catch (A) { } catch (B) { }One handler per type, first match winsUnwinds to the matching blockDifferent handling per type
catch (E e) when (e.Code == x)Catches some instances of a typeUntouched when falseStatus codes, error numbers, retries
when (Log(e)) returning falseSees every exception, catches noneRuns before unwindingLogging without handling
throw;Rethrows the caught exceptionKeptCarry on after logging or cleanup
throw ex;Rethrows as if thrown hereLost below the catchNothing: use throw;
throw new X("…", e)New exception, original as InnerExceptionKept on the inner exceptionAdding context or a domain type
ExceptionDispatchInfo.Capture(e).Throw()Rethrows the same exception laterKept, plus the rethrow siteRethrowing outside the catch

Frequently asked questions

How do I catch multiple exception types in one catch block in C#?

Catch a shared base type and add a filter: catch (Exception e) when (e is FormatException or OverflowException). The or pattern needs C# 9; on C# 6 to 8 write when (e is FormatException || e is OverflowException). The Java syntax catch (FormatException | OverflowException e) does not compile in C#: it fails at the | with a list of errors that includes CS1525, "Invalid expression term '|'".

What is the difference between throw and throw ex in C#?

Both rethrow the same exception object. throw; keeps its stack trace, so it still shows the method where the exception started. throw ex; resets the trace to the line of the throw ex;, so the frames below the catch disappear. Use throw; to rethrow from a catch. If you need to rethrow outside the catch, use ExceptionDispatchInfo.Capture(ex).Throw(), which keeps the original trace too.

Why would you catch an exception and rethrow it?

To do something on the way out and still let the caller see the failure: log it, undo a half-finished change, or record a metric, then throw;. Other good reasons are wrapping it in a more meaningful exception with the original as InnerException, or catching only the cases you can retry. A catch whose only line is throw; changes nothing and can be deleted. If all you want is logging, a filter such as when (Log(e)) that returns false does it without catching at all.

Why does catch (Exception) before catch (FormatException) not compile?

Catch blocks are tried top to bottom, and catch (Exception) matches everything, so the FormatException block after it could never run. The compiler rejects it with error CS0160: "A previous catch clause already catches all exceptions of this or of a super type ('Exception')". Order catch blocks from the most derived type to the most general.

Is a when filter better than catching and checking the type inside?

Usually, yes. A filter decides before the stack unwinds, so an exception it rejects is never caught: the debugger and crash dumps still point at the original throw, and inner finally blocks haven't run yet. Catching and then doing throw; for the types you don't want unwinds first. One detail: if the filter expression itself throws, that exception is swallowed and the filter counts as false, so the original exception goes on to the next handler.

How do I catch several exceptions thrown at once, such as from Task.WhenAll?

await Task.WhenAll(a, b) rethrows only one of the failures. Keep the task returned by WhenAll in a variable and read task.Exception.InnerExceptions in the catch to get all of them. With two tasks that failed with InvalidOperationException and TimeoutException, await threw InvalidOperationException and InnerExceptions listed both. A blocking .Wait() throws an AggregateException that contains every failure.

Should I just catch Exception?

Only where you can deal with any failure: at the top of a program, a request or a background job, to log it and fail cleanly. Elsewhere, catch the types you know how to handle and let the rest propagate, or use a filter to name them. A bare catch { } with no type catches everything as well, and an empty one hides bugs.

Run it yourself

Open any of these in the full C# editor to tweak, run and share.

C# playground