Exception Handling in C#: Clean, Clear, and Effective


Handling errors is not just about using try-catch
. Good error handling is a fundamental part of clean coding: it helps you find bugs faster, avoids unexpected crashes, and makes the system more robust and understandable.
In this article, we’ll see how to write C# code that handles exceptions elegantly and cleanly.
Exceptions Are Not Flow Control
The try-catch
block should not replace normal control flow logic. Using it to handle expected scenarios is considered an anti-pattern.
❌ Bad example:
try
{
var user = users[index];
}
catch (IndexOutOfRangeException)
{
Console.WriteLine("Invalid index.");
}
✅ Better:
csharpCopyEditif (index >= 0 && index < users.Count)
{
var user = users[index];
}
else
{
Console.WriteLine("Invalid index.");
}
Catch Only Exceptions You Can Handle
Avoid using catch (Exception)
unless you have a good reason (e.g., global logging). Only catch specific exceptions that you can truly handle or recover from.
❌ Overly broad:
try
{
// ...
}
catch (Exception ex)
{
Console.WriteLine("Generic error.");
}
✅ More specific:
try
{
var content = File.ReadAllText(path);
}
catch (FileNotFoundException ex)
{
Console.WriteLine($"File not found: {ex.FileName}");
}
Don’t Swallow Exceptions
Silently ignoring exceptions without logging or notification makes debugging nearly impossible.
❌ Avoid this:
try
{
SaveToDatabase(data);
}
catch
{
// Silent failure 😶
}
✅ Better:
try
{
SaveToDatabase(data);
}
catch (Exception ex)
{
logger.LogError(ex, "Error saving data.");
}
Use Custom Exceptions When Appropriate
For domain-specific scenarios, create custom exceptions.
public class InvalidOrderException : Exception
{
public InvalidOrderException(string message) : base(message) { }
}
And use them clearly:
if (!order.IsValid())
throw new InvalidOrderException("The order is not valid.");
Be Careful with Try-Catch Scope
A try block that’s too wide can obscure the origin of the error. Wrap only the code that may actually throw.
❌ Too broad:
try
{
Validate(user);
Save(user); // Only this might fail
}
catch (Exception ex)
{
logger.LogError(ex, "Error.");
}
✅ More precise:
Validate(user);
try
{
Save(user);
}
catch (DbUpdateException ex)
{
logger.LogError(ex, "Error while saving.");
}
Consider the Result Pattern to Avoid Unnecessary Exceptions
For common cases where an operation can fail predictably, avoid exceptions and use a pattern like TryParse
or a custom Result
class.
public class Result<T>
{
public bool Success { get; }
public T Value { get; }
public string Error { get; }
private Result(bool success, T value, string error)
{
Success = success;
Value = value;
Error = error;
}
public static Result<T> Ok(T value) => new(true, value, null);
public static Result<T> Fail(string error) => new(false, default, error);
}
Conclusion
Exceptions are a powerful tool, but when misused, they make code fragile and hard to understand. Clean error handling means:
Don’t overuse exceptions
Use try-catch blocks thoughtfully
Always log meaningful errors
Keep control flow clear and predictable
In the next article, we’ll talk about classes and responsibilities: how to design objects in C# following the Single Responsibility Principle (SRP).
Subscribe to my newsletter
Read articles from Developer Fabio directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Developer Fabio
Developer Fabio
I'm a fullstack developer and my stack is includes .net, angular, reactjs, mondodb and mssql I currently work in a little tourism company, I'm not only a developer but I manage a team and customers. I love learning new things and I like the continuous comparison with other people on ideas.