C# Errors vs Exceptions Performance Benchmarks
Date: 2025-07-30 | benchmarks | build | create | csharp | results |
I've been on an error > exceptions kick the past few months and previously showed that TypeScript exceptions are 300x slower than errors. So here we're doing the same with C#.
Benchmark Setup
We'll be comparing a few common scenarios of error passing using BenchmarkDotNet for benchmarking:
- Throwing exceptions
- Creating exceptions
- Result types
- String types
In each scenario we'll be creating a Guid string to simulate a random error message, constructing the wrapper (if applicable), and doing this 1000 times to get a decent average.
Caveat: All benchmarks have asterisks and these are contrived examples so to truly understand the impact on your system, you'll need to benchmark your system.
Benchmark Results
| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio |
|------------------- |-----------:|---------:|----------:|--------------:|--------:|--------:|----------:|------------:|
| ThrowingExceptions | 4,839.7 us | 95.93 us | 245.91 us | baseline | | 23.4375 | 304.69 KB | |
| CreatingExceptions | 339.6 us | 6.72 us | 17.22 us | 14.29x faster | 1.02x | 17.0898 | 210.94 KB | 1.44x less |
| UsingResultType | 342.3 us | 7.15 us | 20.73 us | 14.19x faster | 1.10x | 9.2773 | 117.19 KB | 2.60x less |
| UsingStringType | 342.7 us | 6.84 us | 16.65 us | 14.16x faster | 0.99x | 7.3242 | 93.75 KB | 3.25x less |
Throwing exceptions is the slowest by 14x and uses at least 40% more memory. This makes sense as it's not only constructing the Guid and Exception but also gathering the stack trace and looking for outer try / catch.
Creating exceptions, using result types, and using raw Guids are all about the same speed which means Guid creation is probably a majority of the time taken. As for memory allocation, Guid is the smallest with Result types ~20% higher and exceptions ~100% higher than Results.
Result Highlights
Result types are efficient. Result types don't seem to add much time or memory overhead which makes them a suitable replacement if you want more ergonomic, type safe representations of errors as values.
Creating exceptions isn't slow, throwing them is. I expected creating exceptions to be nearly as slow as throwing them as that's what I found in my TypeScript exceptions benchmark. However it seems in C# creating exceptions is significantly faster than throwing them. This is likely because C# exceptions copy the stack trace when they're thrown so simply creating an exception doesn't incur this penalty.
Exceptions are for exceptional cases. My recommendation stands that throwing exceptions is good for exceptional cases but a poor choice for anything else. They break control flow, break the type system, and are heavyweight (here 14x slower and 2-3x more memory). Use errors as values instead wherever possible.
Benchmark Code
Here's the benchmark code in its entirety so you can copy it and run it yourself.
If you want the full project files (along with dozens of other example projects), join HAMINIONs to get access to the GitHub repo. Your support helps me create more experiments and reports like this one.
The C# Result type:
public abstract record Result<T, E>;
public record Success<T, E>(T Value) : Result<T, E>;
public record Failure<T, E>(E Error) : Result<T, E>;
Result type examples in other languages:
- TypeScript Result Types - and Why You Should Use Them
- Build a Simple Result type in Python - and why you should use them
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Reports;
namespace ErrorHandlingBenchmark;
[SimpleJob(RuntimeMoniker.Net90)]
[MemoryDiagnoser]
[Config(typeof(Config))]
public class ErrorHandlingBenchmarks
{
private class Config : ManualConfig
{
public Config()
{
SummaryStyle = SummaryStyle.Default.WithRatioStyle(RatioStyle.Trend);
}
}
private readonly int IterationCount = 1000;
[Benchmark(Baseline = true)]
public int ThrowingExceptions()
{
var sum = 0;
for (int i = 0; i < IterationCount; i++)
{
try
{
throw new InvalidOperationException(Guid.NewGuid().ToString());
}
catch (InvalidOperationException e)
{
sum += e.Message.Length;
}
}
return sum;
}
[Benchmark()]
public int CreatingExceptions()
{
var sum = 0;
for (int i = 0; i < IterationCount; i++)
{
var result = new InvalidOperationException(Guid.NewGuid().ToString());
sum += result.Message.Length;
}
return sum;
}
[Benchmark]
public int UsingResultType()
{
var sum = 0;
for (int i = 0; i < IterationCount; i++)
{
var result = new Failure<int, string>(Guid.NewGuid().ToString());
sum += result.Error.Length;
}
return sum;
}
[Benchmark]
public int UsingStringType()
{
var sum = 0;
for (int i = 0; i < IterationCount; i++)
{
var result = Guid.NewGuid().ToString();
sum += -1;
}
return sum;
}
}
Next
Exceptions are exceptional. If you expect an exception it's now an expection (aka not an exception). In those cases use something else to model the error.
If you liked this post you might also like:
Want more like this?
The best way to support my work is to like / comment / share for the algorithm and subscribe for future updates.