"To throw or not to throw, that is the question. Whether 'tis nobler for code to suffer the slings and arrows of unexpected errors, Or to take arms against a sea of troubles and, by opposing, catch them." Hamlet, Act C#, Scene Try-Catch

TL;DR (Summary)

  • Exceptions are costly: Throwing an exception is 50-335 times slower than a simple if or a Try* method.
  • When to use them: For genuinely exceptional errors (network down, file doesn't exist).
  • When NOT to use them: For normal flow control, input validation, or in critical code.
  • Alternatives: Use TryParse, TryGetValue, and similar when you expect frequent errors.
  • Best practice: The mere fact of having try/catch blocks slows down your code by 20%.

What is an exception, really?

In C#, an exception is a mechanism for handling errors in a structured way. Throwing (throw) and catching (try/catch) an exception is more than an if on steroids: it involves creating an object, collecting the stack trace, and abruptly altering the program flow.

That is, it's not free. It's like sending Ophelia to the river with all your error variables - dramatic but costly.

The litmus test: measuring the cost

"Truth is demonstrated with facts, not words," as Polonius would say, so let's put some numbers to the drama of exceptions.

I've prepared this simple battle scenario where an exception faces off against a simple validation: Looking for a key in our dictionary.

[Benchmark]
public void WithSimpleIf()
{
    // Everyday scenario: looking for a value in a dictionary
    var dict = new Dictionary<string, int> { { "one", 1 }, { "two", 2 } };

    if (dict.ContainsKey("three"))
        Console.WriteLine("Found");
    else
        _ = 0; // Do nothing            
}

[Benchmark]
public void WithExceptionWithoutThrowing()
{
    // Same scenario but with try-catch (without actually throwing)
    var dict = new Dictionary<string, int> { { "one", 1 }, { "two", 2 } };

    try
    {
        var value = dict["three"];
        Console.WriteLine("Found");
    }
    catch (KeyNotFoundException) { _ = 0; }           
}

[Benchmark]
public void WithExceptionThrowing()
{
    // Same scenario but throwing exceptions
    var dict = new Dictionary<string, int> { { "one", 1 }, { "two", 2 } };

    try
    {
        throw new KeyNotFoundException();
    }
    catch (KeyNotFoundException) { Console.WriteLine("Exception thrown"); }            
}

[Benchmark]
public void WithTryGetValue()
{
    // The optimal alternative
    var dict = new Dictionary<string, int> { { "one", 1 }, { "two", 2 } };

    if (dict.TryGetValue("three", out var value))
        Console.WriteLine("Found");
    else
        _ = 0; // Do nothing            
}

The results are as dramatic as Ophelia's death:

| Method                       | Mean         | Error         |
|-----------------------------:|-------------:|--------------:|
| WithSimpleIf                 | 0.2989 ms    |  2.4094 ms    |
| WithExceptionWithoutThrowing | 2.0568 ms    | 16.6938 ms    |
| WithExceptionThrowing        | 3.0651 ms    | 24.9719 ms    |
| WithTryGetValue              | 0.2999 ms    |  2.4416 ms    |

By all the ghosts of Elsinore! Let's see what we have here:

  1. WithSimpleIf: Our baseline with a simple if. Efficient and straightforward.
  2. WithExceptionWithoutThrowing: The mere fact of having a try-catch already has a significant cost (588% more), even if the exception is never thrown.
  3. WithExceptionThrowing: 925.4% times slower! Throwing exceptions is a tragedy for performance.
  4. WithTryGetValue: Surprisingly, the TryGetValue pattern is just as fast as the manual if, thanks to internal optimizations.

The difference is enormous: 925.4% slower using exceptions for something as simple as looking up a key in a dictionary.

Why is it so dramatically expensive to throw exceptions?

  • Object creation: The exception is instantiated in memory.
  • Stack trace capture: The CLR traverses the entire call stack.
  • Handler search: The first compatible catch is searched for, unwinding the stack.
  • JIT impact: Breaks optimizations like inlining.
  • Failed branch prediction: Confuses the processor which expects a "normal" flow.
  • GC pressure: More ephemeral objects to collect afterwards.

In summary: throwing exceptions is as costly as a royal parade in the Danish court. Catching them even more so. It's like Hamlet's soliloquy - extensive, dramatic, and consumes many scenic resources. Whereas a simple "if" or a "TryParse" is like the direct "To be or not to be" - concise and effective.

This doesn't mean you should never use exceptions or try/catch. Only that you should use them for what they really are: exceptional and uncontrollable cases in your code.

The original sin: Anti-patterns with exceptions

Something is rotten in Denmark when we see code like this:

public int ParseAge(string value)
{
    try 
    {
        return int.Parse(value);
    }
    catch 
    {
        return 0; // Default value
    }
}

Horror! This anti-pattern is the equivalent of using a sword to open an envelope. What's wrong here?

  1. Using exceptions as normal flow control: Exceptions are for exceptional situations, not for everyday validations.
  2. Generic catch: Catches any exception, not just FormatException.
  3. Hides real problems: If there's a genuine error, it silently ignores it.
  4. Poor performance: Each invalid string will pay the full price of generating and catching an exception.

So should we never use them then?

Not so fast, Horatio!

As I said before, exceptions exist for a reason. Using them properly improves code clarity. Imagine an API:

User GetUserById(string id)

If it doesn't find the user, what does it do?

  • Return null? (Polonius's ghost will appear in your NullReferenceExceptions)
  • An output bool?
  • A TryGetUser pattern?
  • Or throw an exception?

The answer is: it depends on the method's contract.

When to use exceptions (gladly)

  • Exceptional errors: A file that doesn't exist, an index out of range, a network error... These aren't part of the normal program flow. They are exceptions, in every sense of the word.

  • Violated preconditions: If your method clearly specifies that id cannot be null, then ArgumentNullException is appropriate.

  • External validation: If the user enters invalid data, throwing an exception is legitimate.

  • Public APIs: Clarity > performance. It's better to throw ArgumentNullException than to let something explode because of null.

  • For errors that shouldn't be ignored: Errors that require immediate attention.

When to avoid them (for your own good)

  • In high-performance loops: If you have a method that's called 100,000 times per second, you shouldn't use exceptions as flow control. Use TryParse, TryGetValue, TryWhatever.

  • For expected conditions: A user doesn't exist? That's expected. Return null or use a pattern like Result<T>. (In the second part of this article, we'll talk at length about it).

  • In low-level or critical code: Networking, graphics, massive processing... there every millisecond counts.

  • For validating user input: Users rarely enter perfect data. Don't turn each validation into an exception.

  • For controlling program flow: Exceptions are not sophisticated "GoTo's".

The Noble Art of TryParse

Remember our horrible ParseAge example? Here's its redemption:

public int ParseAge(string value)
{
    if (int.TryParse(value, out int result))
        return result;

    return 0; // Default value
}

JUST IN CASE

I know you're an intelligent person and probably already know this, but just in case, TryParse tries to parse the value you pass to the output variable; and will return a boolean indicating whether the conversion was successful or not.

This version:

  • Is orders of magnitude faster
  • Clearly expresses the intention
  • Doesn't abuse the exception system
  • Keeps the stack trace clean

Let's measure the times of these two examples where in one case we use TryParse and in the other we catch the exception when it can't be parsed:

[Benchmark]
public void ParseWithException()
{
    for (int i = 0; i < 1000; i++)
    {
        try { int.Parse("not-a-number"); }
        catch { /* nothing */ }
    }
}

[Benchmark]
public void ParseWithTry()
{
    for (int i = 0; i < 1000; i++)
    {
        int.TryParse("not-a-number", out _);
    }
}

And the benchmark results are:

| Method             | Mean         | Error       |
|-------------------:|-------------:|------------:|
| ParseWithException | 3,612 ms     | 0,07196 ms  |
| ParseWithTry       | 0,00732 ms   | 0,00014 ms  |

Almost 500 times slower with exceptions! It's like comparing the speed of Hamlet deciding what to do with that of lightning!

The Try Family: Your New Best Friends

The .NET Framework is full of Try* methods that help you avoid exceptions:

  • int.TryParse, DateTime.TryParse, etc. - For type conversions
  • Dictionary.TryGetValue - For looking up in collections
  • File.Exists + File.Open - For checking before opening
  • int.TryFormat, DateTime.TryFormat - For formatting values

And many modern APIs follow this pattern. Learn to recognize and love them.

Modern alternatives

These days there are cleaner intermediate solutions than throwing or dying:

public bool TryGetUser(string id, out User user)
{
    user = _repository.Find(id);
    return user != null;
}

Or using methods that return tuples:

public (bool success, User? user) GetUser(string id)
{
    var user = _repository.Find(id);
    return (user != null, user);
}

You can also implement your own Try methods following the established pattern:

public bool TryDivide(int numerator, int denominator, out int result)
{
    if (denominator == 0)
    {
        result = 0;
        return false;
    }

    result = numerator / denominator;
    return true;
}

We still need to mention a very interesting pattern when managing alternatives to throwing exceptions: the Result pattern. But we'll see this in the second part of the article since it has a lot of meat to it.

The Monologue of Destruction: Empty Catch Blocks

"Oh, what a miserable practice is the empty catch! Less bad it is to die than to catch without treating."

The empty catch is probably the most serious sin in exception handling:

try 
{
    // Something dangerous
}
catch { } // Horror and desolation!

This pattern is the equivalent of sweeping the dust under the rug. Exceptions happen for a reason, and ignoring them all is inviting disaster. If such code exists in your applications, it's what's commonly called a textbook Code smell.

At the very least:

try 
{
    // Something dangerous
}
catch (Exception ex)
{
    _logger.Error($"Operation failed: {ex.Message}");
    // Perhaps rethrow something appropriate
}

Final recommendations - The Exception's Soliloquy

  • Don't be afraid to throw exceptions, but do it thoughtfully.
  • Never use exceptions for normal flow control.
  • In hot-path code, avoid throwing, even avoid try/catch if possible.
  • Prefer TryXxx type methods when you expect common errors.
  • When you catch exceptions, do so specifically.
  • Don't leave empty catch blocks. At least log the error.
  • Use finally to ensure resource cleanup.

And if you find yourself writing something like:

try
{
    return Parse(input);
}
catch
{
    return defaultValue;
}

... there's probably a TryParse waiting to be used, and Yorick will be turning in his grave.

Conclusion

Throwing exceptions in C# isn't the end of the world. But it's not free either. It's a powerful tool, useful for communicating real errors, not for validating if a string is a number.

As Hamlet taught us, indecision can be fatal. But in programming, deciding correctly when to throw an exception can be the difference between an application that shines on stage or one that dies in the third act.

My final advice? Use exceptions like Shakespeare's poisoned daggers: reserved for truly dramatic moments, not for every little skirmish in your code.


If you found this article interesting, don't miss the second act where I'll try to explain the differences between throw;, throw ex; and throw new Exception(); The Art of Rethrowing Exceptions: The Three Fates of an Error