Updated on

To catch multiple exceptions in C#, we either give each exception type its own catch block or catch them all in one block with a when filter, such as catch (Exception ex) when (ex is FormatException or OverflowException).

Which one we pick depends on whether each exception needs its own reaction.

To download the source code for this article, you can visit our GitHub repository.

Let’s start.

How Do We Use Multiple Catch Blocks in C#?

If we want to handle the exceptions separately and make a particular decision for each one, we should have specific information about the exceptions that may occur. In this case, the best way is to use an individual catch block for each exception. 

To be precise, let’s suppose we want to take two numbers from the user and then divide the first one by the other. Due to the string format of the input values, we should convert them to a numerical form. But if something goes wrong, we want to show the end-user a reason for the failed operation.

Let’s create the MultipleCatches method to do that. Like every method in this article, it goes inside a class, which we call CatchExamples:

public static int MultipleCatches(string numeratorParam, string denominatorParam)
{
    try
    {
        var numerator = Convert.ToUInt32(numeratorParam);
        var denominator = Convert.ToUInt32(denominatorParam);

        return (int)(numerator / denominator);
    }
    catch (FormatException)
    {
        Console.WriteLine("Format Exception!");
    }
    catch (DivideByZeroException)
    {
        Console.WriteLine("Divide By Zero Exception!");
    }
    catch (OverflowException)
    {
        Console.WriteLine("Overflow Exception!");
    }

    return -1;
}

While converting parameters to an unsigned integer type with the Convert.ToUInt32 method, our application may throw two exceptions FormatException or OverflowException. The division line also may throw a DivideByZeroException. So, we’ve handled all the potential exceptions that can happen in this case.

Only one of these blocks runs for any exception: the runtime checks them from top to bottom and stops at the first type that matches.

In What Order Should Catch Blocks Go?

Since only the first matching block runs, the order matters. We put the most specific exception types first and the general ones last.

Don’t copy this one: it puts Exception above FormatException, so the second block could never run:

try
{
    Console.WriteLine(Convert.ToUInt32("x"));
}
catch (Exception)
{
    Console.WriteLine("general");
}
catch (FormatException)
{
    Console.WriteLine("specific");
}

The compiler refuses it:

error CS0160: A previous catch clause already catches all exceptions of this or of a super type ('Exception')

Swapping the two blocks fixes it. There is one exception to the rule: if the general block has a when filter, it may come first, because it no longer catches everything.

For the basics of how try, catch and finally fit together, check out our article on the try-catch block in C#.

How Do We Catch Multiple Exception Types in One Catch Block?

C# has no syntax for listing several types in one catch clause. Java’s catch (A | B e) form does not compile in C#, so we catch a common base type and narrow it down with an exception filter.

The filter is a when clause after the catch declaration. It holds a Boolean expression, and the block runs only when that expression is true.

With the or pattern combinator from C# 9, the list of types reads almost like the Java version: when (ex is FormatException or DivideByZeroException or OverflowException). The older form, ex is FormatException || ex is DivideByZeroException || ex is OverflowException, does exactly the same and still compiles.

We use this model when every exception in the list gets the same reaction, such as logging it and returning a fallback value.

Any exception that fails the filter is left alone. It keeps travelling to the next catch clause, or up the call stack, as if this block were not there.

Let’s create a SingleCatchWithWhen method to explore this model:

public static int SingleCatchWithWhen(string numeratorParam, string denominatorParam)
{
    try
    {
        var numerator = Convert.ToUInt32(numeratorParam);
        var denominator = Convert.ToUInt32(denominatorParam);

        return (int)(numerator / denominator);
    }
    catch (Exception ex) when (ex is FormatException or DivideByZeroException or OverflowException)
    {
        return -1;
    }
}

In this method, we pack all the potential exceptions into one catch block using the when keyword. It is a shorter way to declare exceptions to the catch block.

The when keyword can test more than the exception’s type, for example its message or a status code, and our article on the when keyword in C# covers filters on the message.

How Do We Tell the Exceptions Apart Inside One Catch Block?

To show the way to separate exceptions in a single catch block, let’s create the SingleCatchSwitchCase method:

public static int SingleCatchSwitchCase(string numeratorParam, string denominatorParam)
{
    try
    {
        var numerator = Convert.ToUInt32(numeratorParam);
        var denominator = Convert.ToUInt32(denominatorParam);

        return (int)(numerator / denominator);
    }
    catch (Exception ex)
    {
        switch (ex)
        {
            case FormatException:
                Console.WriteLine("Format Exception!");
                break;
            case DivideByZeroException:
                Console.WriteLine("Divide By Zero Exception!");
                break;
            case OverflowException:
                Console.WriteLine("Overflow Exception!");
                break;
            default:
                throw;
        }
    }

    return -1;
}

We catch all the exceptions in a single catch block and separate them using a switch-case pattern. Fortunately, switch (ex) can recognize the type of the ex variable and compare it with each case. On the other hand, we can also use the if-else pattern instead of a switch-case model.

Mind you, catch (Exception ex) catches every exception before the switch ever looks at it. Without the default case, any type we didn’t list would be caught and silently dropped. throw; rethrows it and keeps its original stack trace, which throw ex; would not. We cover that difference in throw vs throw ex in C#.

To separately manage all exceptions in one catch block, we can utilize the switch pattern syntax too.

So, let’s create the SingleCatchSwitchPattern method to explain it:

public static int SingleCatchSwitchPattern(string numeratorParam, string denominatorParam)
{
    try
    {
        var numerator = Convert.ToUInt32(numeratorParam);
        var denominator = Convert.ToUInt32(denominatorParam);

        return (int)(numerator / denominator);
    }
    catch (Exception ex)
    {
        string str = ex switch
        {
            FormatException => "Format Exception!",
            DivideByZeroException => "Divide By Zero Exception!",
            OverflowException => "Overflow Exception!",
            _ => "Unknown Exception!"
        };

        Console.WriteLine(str);
    }

    return -1;
}

In our method, each arm of the switch expression matches a type pattern, and the _ discard arm matches everything else. We use this model to generate an appropriate message for each exception. In the str variable, we keep the selected string to print on the console.

The _ arm has the same effect as a missing default: this version catches and keeps every exception, including the ones we never planned for. That is fine for a demo that only prints a message, and worth a second look in real code.

Should We Use Multiple Catch Blocks or a Single Catch Block?

We pick by what each exception needs.

When every exception gets its own reaction, such as a different message or a retry, separate catch blocks say that most clearly. The compiler checks their order for us, too.

When several exceptions get the same reaction, one catch block with a when filter saves us from copying the same handler three times. It also touches only the types it names.

The variant to handle with care is catch (Exception ex) with a switch inside. It catches everything first and decides afterwards, so a type without a matching case is swallowed unless we rethrow it.

The two single-block styles also differ at run time. The runtime evaluates a filter before it unwinds the stack. When a filter returns false, the exception and its stack trace stay as they were, and the finally blocks further down have not run yet. A catch block only ever runs after the stack has unwound.

Microsoft’s C# language reference puts the difference in one line: “The filter expression is evaluated before the stack is unwound.”

We can see it with a small experiment. A method throws from inside a try with a finally, and the caller catches the exception with a filter that records when it runs:

public static List<string> FilterRunsBeforeUnwinding()
{
    var steps = new List<string>();

    try
    {
        ThrowFromInnerMethod(steps);
    }
    catch (FormatException) when (Record(steps, "filter"))
    {
        steps.Add("catch");
    }

    return steps;
}

private static void ThrowFromInnerMethod(List<string> steps)
{
    try
    {
        throw new FormatException();
    }
    finally
    {
        steps.Add("inner finally");
    }
}

private static bool Record(List<string> steps, string step)
{
    steps.Add(step);
    return true;
}

The method returns filter, inner finally, catch, in that order. The filter ran while ThrowFromInnerMethod was still on the stack, before its finally block. Only after the filter said yes did the runtime unwind and enter the catch block. The figure shows the same three steps against the call stack.

Two stacked methods. The exception is thrown in the lower one. Step one, the filter in the upper method runs while the lower method is still on the stack. Step two, the lower method's finally block runs and the method leaves the stack. Step three, the catch block in the upper method runs.

The filter decides first, then the stack unwinds and the catch block runs.

That is why a debugger stopped in a filter still shows the method that threw, and why filters are a good place to log an exception without catching it.

Microsoft also lists performance among the advantages of filters, since an exception no filter accepts is never unwound and caught along the way. We haven’t measured it here, and for code that throws rarely it shouldn’t decide the choice. If you are curious about switch against if-else speed in general, we have a benchmark of the switch and if-else statements.

Here are the four patterns from this article side by side:

PatternA different reaction per type?An exception type we did not listWhat picks the handler, and when our code runs
One catch block per typeYes, each block has its own codeNot caught here, it keeps propagatingThe runtime matches the type, our block runs after the stack unwinds
One catch (Exception ex) with a when filterNo, one shared handlerNot caught here, it keeps propagatingOur filter, before the stack unwinds
catch (Exception ex) with a switch statementYes, one case per typeCaught, then swallowed unless a default: throw; rethrows itOur switch, after the stack unwinds
catch (Exception ex) with a switch expressionYes, one arm per typeCaught by the _ arm and swallowedOur switch, after the stack unwinds

If in doubt, the when filter is the safest of the single-block options, because it only touches the exceptions it names.

Conclusion

So, in this article, we’ve learned how to catch multiple exceptions using different approaches. We’ve seen that we can use the when keyword to achieve that, or catch everything in one block and tell the types apart with the switch statement and the switch expression.

If each exception needs its own handling, separate catch blocks, most specific first, are the clearest choice. If they all get the same handling, a when filter with or patterns catches exactly the types we list and lets everything else pass. For more on pattern syntax like is and or, our pattern matching in C# article goes further.

Tested with .NET 10.0.10 (SDK 10.0.302) and MSTest 4.5.1.