Published
- 4 min read
10 golden rules for working with Exceptions

Today’s issue is brought to you by the Tools and Skills for .NET 8 .
Explore key areas like debugging, testing, and intelligent app development. Strengthening your .NET expertise can open up new opportunities in your career.
Learn more about Tools and Skills for .NET 8Exceptions.
Nobody likes when they occur.
Yet everyone throws them left, right, and center.
Heck, even .NET throws an exception every time you use a particular type incorrectly.
In this email, I’ll review tips, tricks, and best practices when working with exceptions.
Some of them are common sense. Some are neat C# features and recent additions to the .NET ecosystem.
And some are ways to save yourself from bugs caused by exceptions.
Let me show you how to write exceptionally great code. Where exceptions are used, but not abused.
1. Prefer throwing a specific exception
The easiest way to throw an exception is to throw a generic Exception type.
But that’s like telling the doctor:
“I’m in pain. Cure me.”
Instead, when you need to throw an exception, throw a more specific one, for example, ArgumentNullException. Or create a custom exception. Like InvalidUserInputException or OrderNotFoundException.
Your future self will thank you when debugging.
2. Internalise messages in custom exceptions
When you create a custom exception, encapsulate the error message inside it:
public class EntityNotFoundException<T>(long id)
: Exception($"{typeof(T).Name} with id {id} was not found.")
{
}
This ensures that changes to the message format are made in one place.
Rather than scattered across your codebase.
3. ThrowIf methods
A simple way to add a guard clause to the start of your method is to use ThrowIf* family of static methods:
public SearchResult Search(string searchQuery)
{
ArgumentNullException.ThrowIfNull(searchQuery);
// Method logic here
}
Some key .NET exception types have static throw helper methods that allocate and throw the exception.
Here is the list:
- ArgumentNullException.ThrowIfNull
- ArgumentException.ThrowIfNullOrEmpty
- ArgumentException.ThrowIfNullOrWhiteSpace
- ArgumentOutOfRangeException.ThrowIfZero
- ArgumentOutOfRangeException.ThrowIfNegative
- ArgumentOutOfRangeException.ThrowIfEqual
- ArgumentOutOfRangeException.ThrowIfLessThan
- ArgumentOutOfRangeException.ThrowIfNotEqual
- ArgumentOutOfRangeException.ThrowIfNegativeOrZero
- ArgumentOutOfRangeException.ThrowIfGreaterThan
- ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual
4. Throw vs throw ex
When rethrowing an exception, don’t use throw ex.
It wipes out the stack trace and makes debugging a nightmare.
Instead, use throw to preserve the original context.
try
{
// Some logic
}
catch (Exception ex)
{
// Log and rethrow
throw;
}
5. Use catch when expression
Have you ever wanted to handle exceptions only when certain conditions are met?
Starting with C# 6, you can use catch when expression:
catch (FileNotFoundException ex) when (ex.FileName.Contains("importantFile"))
{
// Handle specific case
}
But you can also use it to handle different types of exceptions with one catch block:
try
{
// some logic
}
catch (Exception ex) when (ex is ArgumentNullException || ex is FormatException)
{
// Handle specific exceptions
}
6. Consider using Result for execution flow
Rather than relying on exceptions for control flow, you can use the Result pattern to return success or failure without triggering an exception.
This approach is cleaner and avoids the performance hit of throwing exceptions unnecessarily.
public async Task<Result> Execute(long id)
{
var product = await dbContext.Set<Product>().FindAsync(id);
if (product == null)
{
return Result.Failure(ErrorType.NotFound, $"Product with id {id} was not found");
}
dbContext.Set<Product>().Remove(product);
await dbContext.SaveChangesAsync();
return Result.Success();
}
The downside?
This can get a bit messy when you need to propagate and check for result status within different layers of code.
.NET team is considering adding discriminated unions inside .NET in future versions to support Result type natively.
7. Don’t ignore exceptions
All exceptions should be appropriately logged and handled.
With .NET 8, you can easily centralize your exception handling with a global error handler. This ensures consistent logging and error responses across your application, reducing duplication and improving maintainability. Once you implement and register it, you can simply call app.UseExceptionHandler(); to enable it globally.
Centralize the chaos, simplify the solution.
8. Use finally to release resources
To minimize memory leaks and performance issues while using external resources in your code, such as:
- files
- database
- network connections
You need to release them after you are done.
There are two ways to do it:
- With using statements - for objects that implement IDisposable. This will automatically clean up resources when exceptions are thrown.
- finally block - for resources that don’t implement IDisposable.
try
{
// some operation
}
finally
{
// clean up state in case of errors
}
finally block guarantees that the cleanup code will be executed, even if an exception is thrown.
9. Handle common conditions to avoid exceptions
Not every problem requires an exception.
Handle common conditions proactively to avoid triggering unnecessary exceptions.
For example:
if (fileExists)
{
// Do something
}
else
{
// Handle the condition without throwing an exception
}
Prevention is better than cure.
10. Call Try* methods to avoid exceptions
C# provides many Try* methods that allow you to avoid exceptions by returning a success/failure boolean.
Use these when appropriate to keep your code efficient and clean.
if (int.TryParse(value, out int result))
{
// Do something with result
}
else
{
// Handle failure without throwing an exception
}
There are 3 things certain in life:
- Death,
- Taxes,
- And exceptions in code.
I’m not a doctor.
Or can offer financial advice.
But if you follow the 10 golden rules above, managing exceptions will be easier.