Question
In C#, it is generally discouraged to catch System.Exception because code should usually catch only the exceptions it actually expects and knows how to handle.
This can sometimes lead to repetitive code. For example:
try
{
WebId = new Guid(queryString["web"]);
}
catch (FormatException)
{
WebId = Guid.Empty;
}
catch (OverflowException)
{
WebId = Guid.Empty;
}
Is there a way to catch both exceptions while running the recovery code only once?
The example above is simple because it only deals with a Guid, but imagine a larger block of code where an object is modified several times, and if one of the expected operations fails, you want to reset that object. At the same time, if an unexpected exception occurs, it should still be allowed to propagate upward rather than being silently handled.
Short Answer
By the end of this page, you will understand how to handle multiple expected exceptions in C# without duplicating code. You will learn when to use multiple catch blocks, exception filters, and safer alternatives such as TryParse-style methods that avoid exceptions entirely for expected invalid input.
Concept
In C#, each catch block handles one exception type at a time. If multiple exception types should trigger the same recovery logic, beginners often duplicate the same code in several catch blocks.
That works, but it can become noisy and harder to maintain.
The key idea is this:
- Catch only the exceptions you expect and can handle
- Let unexpected exceptions continue upward
- Prefer APIs that avoid exceptions for normal invalid input
In the Guid example, invalid input is not really an exceptional system failure. It is often just bad user input. In cases like that, a parsing method such as Guid.TryParse is usually better than using exceptions for control flow.
When you truly do need exception handling, C# gives you a few clean options:
- Separate
catchblocks with shared logic moved elsewhere - A single
catchwith an exception filter usingwhen - Refactoring the risky code into smaller operations
Why this matters in real programming:
- It reduces duplicated recovery code
- It makes error handling easier to read
- It avoids hiding real bugs behind broad exception handling
- It encourages use of safer APIs for expected failure cases
Mental Model
Think of exception handling like airport security.
- A known issue is something the staff is trained for, like a passenger carrying too much liquid.
- An unknown issue is something unusual that must be escalated.
You do not want security to treat every problem the same way. Instead:
- known problems get a standard response
- unknown problems get passed to someone higher up
In C#, that means you should catch only the exception types you expect. If two known problems should lead to the same action, you can route both to the same response instead of writing the same recovery code twice.
Syntax and Examples
Basic approach: separate catch blocks
A straightforward way is to keep separate catch blocks:
try
{
WebId = new Guid(queryString["web"]);
}
catch (FormatException)
{
WebId = Guid.Empty;
}
catch (OverflowException)
{
WebId = Guid.Empty;
}
This is valid C#, but the repeated assignment is not ideal.
Better approach for this case: Guid.TryParse
For parsing input, use TryParse when available:
if (!Guid.TryParse(queryString["web"], out var webId))
{
WebId = Guid.Empty;
}
else
{
WebId = webId;
}
This is cleaner because:
- no exceptions are thrown for invalid input
- the intent is obvious
- it is usually more efficient than throwing and catching exceptions
You can shorten it further:
WebId = Guid.TryParse(queryString["web"], out var webId)
? webId
: Guid.Empty;
Step by Step Execution
Consider this example:
string input = "not-a-guid";
Guid webId;
if (Guid.TryParse(input, out var parsedId))
{
webId = parsedId;
}
else
{
webId = Guid.Empty;
}
Step-by-step
inputis assigned the value"not-a-guid".Guid.TryParse(input, out var parsedId)tries to convert that string to aGuid.- The conversion fails because the text is not a valid GUID format.
TryParsereturnsfalseinstead of throwing an exception.- The
elseblock runs. webIdis set toGuid.Empty.
Now compare that with exception-based code:
string input = "not-a-guid";
Guid webId;
try
{
webId = new Guid(input);
}
catch (Exception ex) when (ex is FormatException || ex OverflowException)
{
webId = Guid.Empty;
}
Real World Use Cases
1. Parsing user input
A web app receives values from query strings, forms, or JSON payloads.
if (!int.TryParse(form["age"], out var age))
{
age = 0;
}
2. Resetting state after expected validation failures
A service updates an object in several steps. If a known validation-related exception occurs, it resets the object to a safe state.
try
{
ApplyUserChanges(profile);
}
catch (Exception ex) when (ex is FormatException || ex is InvalidOperationException)
{
profile.Reset();
}
3. File and configuration handling
Applications often treat some failures as expected, such as invalid config values.
try
{
settings.Port = int.Parse(config["Port"]);
}
catch (Exception ex) when (ex is FormatException || ex is OverflowException)
{
settings.Port = 8080;
}
4. API input validation
Backend code commonly validates IDs, dates, and numeric values from requests. For normal invalid input, developers usually prefer validation or TryParse instead of exceptions.
Real Codebase Usage
In real C# codebases, developers usually follow these patterns:
Prefer TryParse for expected invalid input
If bad input is normal, avoid exceptions:
if (!DateTime.TryParse(request.DateText, out var date))
{
return BadRequest("Invalid date.");
}
Use guard clauses early
Reject bad values before deeper logic runs:
if (string.IsNullOrWhiteSpace(queryString["web"]))
{
WebId = Guid.Empty;
return;
}
Use exception filters for shared handling
When multiple exception types need the same response, filters reduce duplication:
catch (Exception ex) when (ex is FormatException || ex is OverflowException)
{
ResetObject();
}
Keep try blocks small
A small try block makes it clearer which code might fail and which exceptions are expected:
try
{
WebId = Guid(queryString[]);
}
(Exception ex) (ex FormatException || ex OverflowException)
{
WebId = Guid.Empty;
}
Common Mistakes
Catching Exception without a good reason
Broken example:
try
{
WebId = new Guid(queryString["web"]);
}
catch (Exception)
{
WebId = Guid.Empty;
}
Why it is a problem:
- it may hide unrelated bugs
- it makes debugging harder
- it handles errors you did not intend to handle
Using exceptions for normal control flow
Broken example:
try
{
age = int.Parse(input);
}
catch (FormatException)
{
age = 0;
}
Better:
age = int.TryParse(input, out var parsedAge) ? parsedAge : 0;
Putting too much code inside one try
Broken example:
try
{
LogRequest();
WebId = new Guid(queryString["web"]);
SaveToDatabase();
}
catch (Exception ex) (ex FormatException || ex OverflowException)
{
WebId = Guid.Empty;
}
Comparisons
| Approach | Best for | Pros | Cons |
|---|---|---|---|
Multiple catch blocks | A few exception types with distinct actions | Clear and explicit | Can duplicate code |
Exception filter with when | Multiple exception types with the same handling | Reduces duplication, keeps unexpected exceptions unhandled | Slightly more advanced syntax |
TryParse | Expected invalid input | No exceptions, clearer intent, efficient | Only works when the API provides it |
Catch Exception | Rare top-level boundaries like app startup or logging | Can prevent app crash at system boundaries | Too broad for normal business logic |
new Guid(...) vs
Cheat Sheet
Quick rules
- Catch only exceptions you expect and can handle.
- Do not catch
Exceptionjust to avoid writing more specific code. - For parsing user input, prefer
TryParseover exceptions. - Keep
tryblocks small. - Use exception filters with
whenif multiple exception types share the same handling.
Common patterns
Preferred for parsing
WebId = Guid.TryParse(queryString["web"], out var webId)
? webId
: Guid.Empty;
Exception filter
try
{
WebId = new Guid(queryString["web"]);
}
catch (Exception ex) when (ex is FormatException || ex is OverflowException)
{
WebId = Guid.Empty;
}
Separate catch blocks
try
{
DoWork();
}
catch (FormatException)
{
ResetObject();
}
catch (InvalidOperationException)
{
ResetObject();
}
Edge cases
FAQ
Can C# catch multiple exceptions in one catch block?
Yes. A common modern approach is an exception filter:
catch (Exception ex) when (ex is FormatException || ex is OverflowException)
Is it bad to catch System.Exception in C#?
Usually yes for normal application logic, because it is too broad. It is sometimes acceptable at top-level boundaries such as application startup, logging, or global error handling.
What is better than catching parsing exceptions in C#?
Use TryParse methods when available, such as int.TryParse, DateTime.TryParse, or Guid.TryParse.
Should I use new Guid(string) or Guid.TryParse?
For user input or external input, prefer Guid.TryParse. It is clearer and avoids exception-based control flow.
Will exception filters hide unexpected exceptions?
No. If the filter condition does not match, the exception is not handled by that catch block and continues upward.
When should I keep separate catch blocks instead of using a filter?
Mini Project
Description
Build a small C# input parser that reads a user-provided string and safely converts it into a Guid. The project demonstrates the difference between exception-based parsing and TryParse, and shows how to provide a fallback value when input is invalid.
Goal
Create a program that attempts to read a GUID string and stores either the parsed value or Guid.Empty if the input is invalid.
Requirements
- Read a string value that represents a possible GUID.
- Attempt to convert it to a
Guidsafely. - If the conversion fails, store
Guid.Empty. - Print the final
Guidvalue. - Use a clean approach suitable for expected invalid input.
Keep learning
Related questions
AddTransient vs AddScoped vs AddSingleton in ASP.NET Core Dependency Injection
Learn the differences between AddTransient, AddScoped, and AddSingleton in ASP.NET Core DI with examples and practical usage.
C# Type Checking Explained: typeof vs GetType() vs is
Learn when to use typeof, GetType(), and is in C#. Understand exact type checks, inheritance, and safe type testing clearly.
C# Version Numbers Explained: C# vs .NET Framework and Why “C# 3.5” Is Incorrect
Learn the correct C# version numbers, how they map to .NET releases, and why terms like C# 3.5 are inaccurate and confusing.