"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");
1,000,000 throws: 959 ms
one throw: 959 ns
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");
100 million calls each:
no try: 82 ms
with try: 102 ms
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);
}
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
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)");
Parse + catch: 1434 ms (1,000,000 bad)
TryParse: 3 ms (1,000,000 bad)
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; }
}
The top two frames of each stack trace:
throw ex: X5.WithThrowEx <- X5.Run
throw : X5.SaveOrder <- X5.WithThrow
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
Recap
- One throw costs about a microsecond. Fine for real errors, not for normal control flow.
- A
tryblock costs almost nothing when nothing throws (about 0.2 ns per call here). - Depth barely mattered. Throw less, not shallower.
- Expected failures belong in
TryParse/TryGetValue, not incatch. - Rethrow with
throw;, neverthrow 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)