DEV Community

Cover image for Are exceptions really slow in C#? I threw a million to find out
Naze Code
Naze Code

Posted on

Are exceptions really slow in C#? I threw a million to find out

"Exceptions are slow" gets repeated in every C# code review, usually without a number attached. So I measured it. Five experiments, each run three times on an idle machine: .NET 10.0.10, Windows 11, Release build, after a warm-up. The ranges below are from those three runs.

Absolute timings depend a lot on the machine and what else it's doing (the runs in the video below were about twice as slow as these), so focus on the ratios.

1. What does one throw cost?

const int count = 1_000_000;
var timer = Stopwatch.StartNew();

for (var i = 0; i < count; i++)
{
    try { throw new InvalidOperationException("nope"); }
    catch (InvalidOperationException) { }
}

Console.WriteLine($"{count:N0} throws: {timer.ElapsedMilliseconds} ms");
Console.WriteLine($"one throw: {timer.Elapsed.TotalNanoseconds / count:N0} ns");
Enter fullscreen mode Exit fullscreen mode
1,000,000 throws: 959 ms
one throw: 959 ns
Enter fullscreen mode Exit fullscreen mode

About 1 microsecond per throw and catch (950–963 ns over three runs). For a real error that happens once in a while, that's nothing. For something that happens on every request, it adds up fast.

2. Does a try block cost anything if nothing throws?

var timer = Stopwatch.StartNew();
for (var i = 0; i < count; i++) sink += Work(i);
Console.WriteLine($"no try:   {timer.ElapsedMilliseconds} ms");

timer.Restart();
for (var i = 0; i < count; i++)
{
    try { sink += Work(i); }
    catch (Exception) { sink--; }
}
Console.WriteLine($"with try: {timer.ElapsedMilliseconds} ms");
Enter fullscreen mode Exit fullscreen mode

100 million calls each:

no try:   82 ms
with try: 102 ms
Enter fullscreen mode Exit fullscreen mode

Three runs: 81–82 ms vs 102–103 ms. So in this tight loop the try version was consistently about 20 ms slower over 100 million calls, which is about 0.2 nanoseconds per call.

That's not zero, and the video called it free, which these runs don't support. But compare it with part 1: one throw costs about 5,000 times more than entering a try. .NET doesn't run any setup code for a try block; the runtime keeps a table of protected regions and only looks at it when something is thrown. Wrapping code in try is not where your time goes.

3. Does it matter how deep the throw is?

static void Dive(int depth)
{
    if (depth == 0) throw new InvalidOperationException("bottom");
    Dive(depth - 1);
}
Enter fullscreen mode Exit fullscreen mode

Throwing from 1, 10 and 50 calls deep, 100,000 times each:

depth  1: 1,176 ns per throw
depth 10: 1,163 ns per throw
depth 50: 1,181 ns per throw
Enter fullscreen mode Exit fullscreen mode

Across three runs, all depths stayed between 1,163 and 1,215 ns, with no trend. At these depths, the number of throws matters, not how far they travel.

4. int.Parse + catch vs int.TryParse

A million strings that aren't numbers:

var timer = Stopwatch.StartNew();
var bad = 0;
foreach (var text in Inputs)
{
    try { int.Parse(text); }
    catch (FormatException) { bad++; }
}
Console.WriteLine($"Parse + catch: {timer.ElapsedMilliseconds} ms ({bad:N0} bad)");

timer.Restart();
bad = 0;
foreach (var text in Inputs)
    if (!int.TryParse(text, out _)) bad++;
Console.WriteLine($"TryParse:      {timer.ElapsedMilliseconds} ms ({bad:N0} bad)");
Enter fullscreen mode Exit fullscreen mode
Parse + catch: 1434 ms (1,000,000 bad)
TryParse:      3 ms (1,000,000 bad)
Enter fullscreen mode Exit fullscreen mode

Same answer, roughly 480 times faster. This is where "exceptions are slow" is true in practice: using them for an outcome you expect. Bad user input, a missing key or a cache miss isn't exceptional. Use the Try pattern for those.

5. throw ex vs throw

Not a speed issue, but the most common exception mistake I see:

static void WithThrowEx()
{
    try { SaveOrder(); }
    catch (Exception ex) { throw ex; }
}

static void WithThrow()
{
    try { SaveOrder(); }
    catch (Exception) { throw; }
}
Enter fullscreen mode Exit fullscreen mode

The top two frames of each stack trace:

throw ex: X5.WithThrowEx  <-  X5.Run
throw   : X5.SaveOrder  <-  X5.WithThrow
Enter fullscreen mode Exit fullscreen mode

throw ex restarts the stack trace at the catch, so SaveOrder, the method that actually failed, has disappeared. throw; keeps it. A default dotnet build already warns about it:

warning CA2200: Re-throwing caught exception changes stack information
Enter fullscreen mode Exit fullscreen mode

Recap

  1. One throw costs about a microsecond. Fine for real errors, not for normal control flow.
  2. A try block costs almost nothing when nothing throws (about 0.2 ns per call here).
  3. Depth barely mattered. Throw less, not shallower.
  4. Expected failures belong in TryParse / TryGetValue, not in catch.
  5. Rethrow with throw;, never throw ex;.

Run these on your machine and post your numbers in the comments. I'm curious how much they vary.

I make short, tested videos about C# and what happens when you measure it. More on the Naze Code YouTube channel.


Sources: Microsoft Learn, Best practices for exceptions, Exceptions and performance, CA2200. Tested on .NET 10.0.10, Windows 11, Release build, 3 runs each.

Top comments (0)