1. Introduction
Once your program gets a bit more complicated than just Hello, world!, you quickly run into the need to handle different errors in different ways. Imagine: you're working with files, networks, databases — and for each error scenario, you want a different reaction. For example, if a file is missing, you might want to let the user pick another one, but if there's a network error — maybe retry or show a friendly message like "Check the cable, maybe your cat chewed it again."
In C#, you use multiple catch blocks in a row for this. Each block specializes in its own exception type (and its descendants).
Structure of multiple catch
try
{
// Here — code that might throw different exceptions
}
catch (FileNotFoundException ex)
{
Console.WriteLine($"File not found: {ex.Message}");
}
catch (IOException ex) // catches all IO errors, unless it's FileNotFoundException
{
Console.WriteLine($"Input/output error: {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"Something went wrong: {ex.Message}");
}
Important! The compiler goes through the blocks from top to bottom and picks the first one that matches the type. If it catches a FileNotFoundException, it won't go further down the chain.
Illustration of the "chain"
| Exception type | Which block catches it? |
|---|---|
| FileNotFoundException | The first catch |
| IOException (other) | The second catch |
| ArgumentException | The third (general) catch |
Why can't you mix them up?
The "widest" catch — like catch (Exception) — should always go last, otherwise it'll "swallow" all exceptions too early, and the specialized catch blocks won't get a chance.
It's like turning off all the fire alarms in a building just because one went off from a burnt toaster: if there's a real fire later, the system won't notice.
Working example
using System;
using System.IO;
class Program
{
static void Main()
{
try
{
double result = CalculateAverageAgeFromFile("users.txt");
Console.WriteLine($"Average age — {result}");
}
catch (FileNotFoundException ex)
{
Console.WriteLine("Error: file not found. Check the path.");
}
catch (FormatException ex)
{
Console.WriteLine("Data error: can't read age.");
}
catch (Exception ex)
{
Console.WriteLine($"Other error: {ex.Message}");
}
}
static double CalculateAverageAgeFromFile(string filePath)
{
// (Implementation: reads file, parses ages, calculates average)
// ...
throw new NotImplementedException();
}
}
Here we clearly separate what to do if the file is missing (FileNotFoundException), and what to do if the file has some "weird" data (FormatException). All other cases are handled by the "backup" third block.
2. catch Filters: catching subtle stuff
Multiple catch blocks are handy, but sometimes that's not enough. Sometimes, even within one exception type, you want to react differently depending on the circumstances.
For example, if the network isn't working because the internet ran out — maybe we can try to reconnect. But if the server just isn't responding — maybe we should show a different message.
That's where catch filters come in — a real C# superpower that lets you catch not just by type, but also by extra conditions.
Syntax for the when filter
catch (IOException ex) when (ex.Message.Contains("no disk"))
{
Console.WriteLine("Oops! Looks like you yanked out the flash drive.");
}
catch (IOException ex)
{
Console.WriteLine("Other input/output error: " + ex.Message);
}
Here, the first block only catches those IOException where the message contains the phrase "no disk" — everything else goes to the second block.
Real-life usage
Filters are especially useful for network errors, when you need to decide what to do based on the inner properties of the exception: retry, or just show an error.
Another example: imagine in a method that parses a file, you want not just a generic FormatException, but a special case — if it's about reading the age (like if the age is written as "abc" instead of a number).
catch (FormatException ex) when (ex.Message.Contains("age"))
{
Console.WriteLine("Error: couldn't read age. Check your data.");
}
catch (FormatException ex)
{
Console.WriteLine("Data format error: " + ex.Message);
}
Filters and performance
Filters are super handy, but remember that the filter condition is evaluated before entering the catch block, so if it doesn't match — the block body isn't touched at all.
By the way, if the filter itself throws an exception (like if your when expression divides by zero) — that exception will never be caught by this catch block. So be careful with that.
3. Combining multiple catch and filters
Imagine in your project you need to handle not just the exception type, but also its internal state. For example, when working with IOException, you want different behavior if the file system error is about access or about running out of disk space.
try
{
File.AppendAllText("log.txt", "New entry\n");
}
catch (IOException ex) when (ex.Message.Contains("No space"))
{
Console.WriteLine("Error: Disk is out of space!");
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("Error: no permission to write file. Try running as admin.");
}
catch (IOException ex)
{
Console.WriteLine("Other disk error: " + ex.Message);
}
Here we use both a filter and type-based error separation. This kind of flexibility is especially valuable when building a complex app where you want to react informatively to frequent failures.
4. Features and "gotchas" of filters and multiple catch
- A catch filter can use the exception variable (ex), but can't change it.
- If you throw an exception inside when, it won't be "caught" by this catch — it'll go up the stack as if the filter wasn't there.
- The filter logic becomes part of the contract: your teammate needs to know that not every IOException will be caught — only if the condition matches.
- If you use just one catch for the whole method, but with a filter inside, there's a chance you'll miss some "extra" exceptions. So if in doubt — use explicit, separate blocks.
- In big apps, you can use filters for centralized logic, like logging only critical errors.
5. Scenarios for interviews and real work
Knowing filters and multiple catch blocks isn't just for style points or a high clean code score. It's a real skill you might be asked about in interviews, especially if the job involves supporting a big, complex, distributed app.
Sample questions:
- How do you handle different file reading error scenarios in C#, so that for different situations (no file, disk busy, data corrupted) you show different messages?
- How do you avoid "swallowing" an important error if you put a general catch (Exception) block?
- What's the point of a catch filter? Can you use throw in it?
GO TO FULL VERSION