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

Developer FabioDeveloper Fabio
3 min read

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).

0
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.