It's dangerous to code alone! Take this.

Analyzing the Cost of Exception Handling

Published on 12 Mar 2022.

At work, on a very large project, we did something to see how many exceptions were being thrown (and handled, or the software would crash). We were shocked by the results: 110 exceptions per second were being thrown! Nearly all of these were from one particular spot in the code, and we’re now planning on cleaning that up.

The age-old wisdom is that exception throwing and handling is quite expensive, at least when compared to normal procedural code.

One of my coworkers stated that he “read something somewhere” that indicated that it may be slow, but not slow enough to stop you from using it when the tool is the right one for the job. At a philosophical level, that feels right to me. But it got me curious about the performance around this, and I wanted to give it a try and measure it.

Does adding in try/catch infrastructure slow things down?

My first target was to answer this question:

Forget the actual throwing and handling of exceptions. Does unused try/catch infrastructure slow things down?

My first attempt at this was to compare these two bits of code:

[Benchmark]
public int Plain()
{
    int x = 0;
    x++;
    return x;
}

[Benchmark]
public int WithTryCatch()
{
    try
    {
        int x = 0;
        x++;
        return x;
    }
    catch (Exception)
    {
        return 0;
    }
}

This code should not be throwing any exceptions, so the try/catch is unnecessary, but I wanted to measure just the performance cost of that alone. Here were the results (again, using Benchmark.NET, like last week):

|       Method |      Mean |     Error |    StdDev |    Median |
|------------- |----------:|----------:|----------:|----------:|
|        Plain | 0.0007 ns | 0.0025 ns | 0.0023 ns | 0.0000 ns |
| WithTryCatch | 0.0652 ns | 0.0026 ns | 0.0024 ns | 0.0653 ns |

// * Warnings *
ZeroMeasurement
  ExceptionPerformance.Plain: Default -> The method duration is indistinguishable from the empty method duration

There isn’t a whole lot of code in here. But the try/catch block took us from 0.7 picoseconds, to 65.2 picoseconds–93x longer.

That note at the end is interesting. Benchmark.NET claims it can’t tell a difference between Plain and an empty method.

I did wonder if the compiler had optimized the entire method down to something like a single return 1;. The main C# compiler, however, did not. The IL instructions do what you’d expect. However, the JIT compiler may be optimizing even further when it compiles to hardware-level instructions.

So I decided to try a different sample that I felt more confident couldn’t possibly be simply optimized away:

public class ExceptionPerformance
{
    public static int A = 2;
    public static int B = 3;

    [Benchmark]
    public int Plain()
    {
        return A + B;
    }

    [Benchmark]
    public int WithTryCatch()
    {
        try
        {
            return A + B;
        }
        catch (Exception)
        {
            return 0;
        }
    }
}

This code now uses public, mutable fields. At this point, I can’t see how a compiler–C# or JIT–would dare do any optimizations, and would need to do the actual computation. The results:

|       Method |      Mean |     Error |    StdDev |    Median |
|------------- |----------:|----------:|----------:|----------:|
|        Plain | 0.0001 ns | 0.0002 ns | 0.0002 ns | 0.0000 ns |
| WithTryCatch | 0.2785 ns | 0.0036 ns | 0.0031 ns | 0.2781 ns |

// * Warnings *
ZeroMeasurement
  ExceptionPerformance.Plain: Default -> The method duration is indistinguishable from the empty method duration

It’s the same story, though it is a bit surprising that Plain got faster and WithTryCatch got slower. But we’re talking tiny numbers here, and I’m not sure I’d read too much into that.

So I think this answers the question.

There’s a bit of overhead to an unused try/catch block, but it is well under a nanosecond. I think I’d argue that a more compelling reason to skip an unused try/catch block is the cost to readability, rather than the CPU cost. Our Plain method could have actually looked like this, had we wanted: public int Plain() => A + B;. A single line, not three.

But the cost of try/catch exists, but it is quite minimal. So it’s fine to use it when you need to handle potential errors.

What is the cost of throwing an exception in a single method?

The next thing to check was the cost of actually throwing an exception. Here’s my code for that:

public class ExceptionPerformance
{
    public static int A = 2;
    public static int B = 3;

    [Benchmark]
    public int NoThrow()
    {
        try
        {
            return A + B;
        }
        catch (Exception)
        {
            return A + B; // Never runs, but here for symmetry.
        }
    }

    [Benchmark]
    public int Throw()
    {
        try
        {
            throw new Exception();
        }
        catch (Exception)
        {
            return A + B;
        }
    }
}

And here are the results:

|  Method |          Mean |       Error |      StdDev |
|-------- |--------------:|------------:|------------:|
| NoThrow |     0.2763 ns |   0.0038 ns |   0.0034 ns |
|   Throw | 5,422.6208 ns | 104.2048 ns | 120.0024 ns |

Frankly, that number shocked me. It is 19000 times longer! Our earlier code could be measured in picoseconds, but the version that actually throws an exception is measured in microseconds, 6 orders of magnitude bigger.

This begs the question of if it is the try/catch infrastructure or the memory allocation for new Exception(), so I tweaked my example to make the Exception instance outside of the code and run it again:

private static Exception _exception = new Exception();
[Benchmark]
public int Throw()
{
    try
    {
        throw _exception;
    }
    catch (Exception)
    {
        return A + B;
    }
}
|  Method |          Mean |      Error |     StdDev |
|-------- |--------------:|-----------:|-----------:|
| NoThrow |     0.2989 ns |  0.0110 ns |  0.0103 ns |
|   Throw | 5,369.7277 ns | 50.9397 ns | 45.1567 ns |

The numbers came down slightly, but I’m not sure if that is enough to truly matter. (And it is an awkward construction anyway. Nobody makes exceptions to throw ahead of time.)

So the answer here is…

Throwing and catching exceptions does take time–several microseconds on my computer.

What about a deep stack trace?

The last question I wanted to answer was about how this affects stack traces. Suppose, for example, we’ve ran into a problem and need to get up ten method calls on the stack to resolve it. Does throwing exceptions come out better in that situation?

The alternative to throwing an exception but reporting an error is with a return value that can indicate if an error happened. In my code below, I’m assuming that a negative return value indicates an error and the specific number indicates which error, though there are plenty of other ways to do this.

public class ExceptionPerformance
{
    public static int A = 2;
    public static int B = 3;

    [Benchmark]
    public bool ProceduralError()
    {
        bool error = Procedural1() < 0;
        return error;
    }

    private static int Procedural1() => -1;

    [Benchmark]
    public bool ExceptionalError()
    {
        try
        {
            Exceptional1();
            return false;
        }
        catch (Exception)
        {
            return true;
        }
    }

    private static void Exceptional1() => throw new Exception();
}

And the performance results:

|           Method |          Mean |      Error |     StdDev |
|----------------- |--------------:|-----------:|-----------:|
|  ProceduralError |     0.0032 ns |  0.0031 ns |  0.0026 ns |
| ExceptionalError | 6,579.6750 ns | 39.2052 ns | 36.6726 ns |

// * Warnings *
ZeroMeasurement
  ExceptionPerformance.ProceduralError: Default -> The method duration is indistinguishable from the empty method duration

Once again, the procedural version is much faster. Fast enough that Benchmark.NET isn’t even sure there’s actual running code in there.

That’s for a single level of method calls. What if we add a few more?

public class ExceptionPerformance
{
    public static int A = 2;
    public static int B = 3;

    [Benchmark]
    public bool ProceduralError()
    {
        bool error = Procedural10() < 0;
        return error;
    }

    private static int Procedural1() => -1;
    private static int Procedural2() => Procedural1();
    private static int Procedural3() => Procedural2();
    private static int Procedural4() => Procedural3();
    private static int Procedural5() => Procedural4();
    private static int Procedural6() => Procedural5();
    private static int Procedural7() => Procedural6();
    private static int Procedural8() => Procedural7();
    private static int Procedural9() => Procedural8();
    private static int Procedural10() => Procedural9();

    [Benchmark]
    public bool ExceptionalError()
    {
        try
        {
            Exceptional10();
            return false;
        }
        catch (Exception)
        {
            return true;
        }
    }

    private static void Exceptional1() => throw new Exception();
    private static void Exceptional2() => Exceptional1();
    private static void Exceptional3() => Exceptional2();
    private static void Exceptional4() => Exceptional3();
    private static void Exceptional5() => Exceptional4();
    private static void Exceptional6() => Exceptional5();
    private static void Exceptional7() => Exceptional6();
    private static void Exceptional8() => Exceptional7();
    private static void Exceptional9() => Exceptional8();
    private static void Exceptional10() => Exceptional9();
}
|           Method |          Mean |       Error |      StdDev |
|----------------- |--------------:|------------:|------------:|
|  ProceduralError |     0.0000 ns |   0.0000 ns |   0.0000 ns |
| ExceptionalError | 6,989.8619 ns | 124.4855 ns | 116.4438 ns |

// * Warnings *
ZeroMeasurement
  ExceptionPerformance.ProceduralError: Default -> The method duration is indistinguishable from the empty method duration

I’d say that looks like it changed nothing.

And the bottom line here is that yes, exception throwing and catching is way slower than equivalent procedural code when it comes to raw CPU performance in C#.

But at what cost?

Now before you go strip out all your exceptions in your code, there’s one other point I want to make. Code that uses exceptions is often vastly cleaner code. This is especially evident in code that is already trying to return a value. Suppose, before you consider error states, you have this code:

public int GetNumber() { ... }

Which can be used like this, if there’s no error:

int result = GetNumber();

When dealing with an error condition, if you use exceptions, none of this code needs to change (other than some exception handler, somewhere up the call stack).

If you decide to return an error code, you’ve got a problem because this method already returns a value.

You could return a tuple:

public (bool Success, int ValueIfSuccessful) GetNumber() { ... }

And:

var maybeResult = GetNumber();
if (maybeResult.Success)
{
    int result = maybeResult.ValueIfSuccessful;
    // ...
}

Or perhaps you use an output parameter, a la int.Parse and int.TryParse:

public bool GetNumber(out int result) { ... }

And:

bool success = GetNumber(out int result);
if (success)
{
    // ...
}

If the place that detects the error and the place that can correct the error are ten method calls apart, you’re going to have to rework the signature of a whole lot of methods.

So, yes, CPU performance matters, but so does maintainability and readability of the code. Procedural error handling–with an error code return value–beats exceptions when it comes to CPU performance, but the truth is, a few microseconds in the (presumably) very rare case where you’re throwing an exception rarely matters.

The cost of returning an error code does serious damage to the readability and maintainability of the code, and in most situations, that’s the better thing to optimize for.

Summary

Since this was another rather long post, here’s a short summary:

  • Adding in unused exception handling infrastructure is way slower than none at all.
  • Comparatively, throwing and catching exceptions is vastly slower than other means–we saw a 19000x increase.
  • But it is still fast enough for most practical purposes. We’re looking at a few microseconds to throw and catch and exception.
  • Even though they’re slower, exception-based error handling is typically simpler code, and that benefit to readability and maintainability trumps a few microseconds of CPU usage in most situations.