How .NET handles exceptions internally (and why they're expensive)
What really happens when you write throw new Exception() in .NET? Microsoft guidelines state that
When a member throws an exception, its performance can be orders of magnitude slower.
It's not just a simple jump to a catch block, but a lot goes in CLR (Common Language Runtime). Expensive operations such as stack trace capture, heap allocations, and method unwinding occur each time. You will not want to use them in any hot paths. Today, In today's post, I will help you decide when exceptions are appropriate and when a simple alternative type might be better.

What is an Exception?
An exception is an error condition or unexpected behaviour during the execution of a program. Exceptions can occur at runtime for various reasons, such as accessing a null object, dividing by zero, or requesting a file that is not found. A C# exception contains several properties, including a Message describing the cause of the exception. StackTrace contains the sequence of method calls that led to the exception in reverse call order to trace the exception source.
How does an exception work?
The try block encloses the code prone to exceptions. try/catch protects the application from blowing up. Use the throw keyword to signal the error and throw an Exception object containing detailed information, such as a message and a stack trace. The caught exception allows the program to continue gracefully and notify the user where and what error occurred. When an error occurs, the CLR searches for a compatible catch block in the current method. If not found, it moves up the call stack to the calling method, and so on. Once a matching catch is found based on the exception type, control jumps to that block. In an unhandled exception situation where no compatible catch block is found, the application can terminate. Exception handling uses a heap to store the message. To look for a catch body, the CLR unwinds the stack by removing intermediate stack frames. The JIT must generate EH tables and add hidden control-flow metadata.
What is OneOf<T> in .NET?
OneOf<T> or OneOf<T1, T2, T...> represents a discriminated union containing all possible returns of an operation or a method. It contains an array of types, allowing a method to return one of several defined possibilities. The OneOf pattern provides you with fine-grained control and type safety.
Examine Exceptions with the benchmark.
To truly understand it, let's create an application. I will use a console application.
Step 1: Create the project
dotnet new console -n ExceptionBenchmark
cd ExceptionBenchmarkStep 2: Add necessary packages
I am adding the Benchmark library along with OneOf, which is used for the OneOf return type.
dotnet add package BenchmarkDotNet
dotnet add package OneOfStep 3: Set up the program.cs
All the code is in the Program.cs
using System;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using OneOf;
BenchmarkRunner.Run<ExceptionBenchmarks>();
[MemoryDiagnoser]
public class ExceptionBenchmarks
{
private const int Iterations = 100_000;
private const int FailureEvery = 10;
[Benchmark]
public int NoException()
{
int failures = 0;
for (int i = 1; i <= Iterations; i++)
{
if (!DoWork_NoException(i))
failures++;
}
return failures;
}
private bool DoWork_NoException(int i)
{
return i % FailureEvery != 0;
}
[Benchmark]
public int WithException()
{
int failures = 0;
for (int i = 1; i <= Iterations; i++)
{
try
{
DoWork_WithException(i);
}
catch
{
failures++;
}
}
return failures;
}
private void DoWork_WithException(int i)
{
if (i % FailureEvery == 0)
throw new InvalidOperationException();
}
[Benchmark]
public int WithOneOf()
{
int failures = 0;
for (int i = 1; i <= Iterations; i++)
{
var result = DoWork_WithOneOf(i);
if (result.IsT1)
failures++;
}
return failures;
}
private OneOf<Success, Error> DoWork_WithOneOf(int i)
{
if (i % FailureEvery == 0)
return new Error("Error");
return new Success("Passed");
}
private readonly struct Success
{
public string Message { get; }
public Success(string message)
{
Message = message;
}
}
private readonly struct Error
{
public string Message { get; }
public Error(string message)
{
Message = message;
}
}
}The first method is simple with no exception. Then it throws an exception, and in subsequent methods, it finally returns an error object from OneOf. To make it realistic, each method will observe with 10% error and 90% success rate, as FailureEvery is set to 10. Success and Error are value types to avoid allocations, since they only return the value from the method.
Step 4: Run and test
dotnet run -c Release
The best performer is the NoException. But that is not practical, you have to identify unexpected behaviour and report it in the code flow. Firstly, a naive approach is to use an exception. Using it adds a time cost and increases the Garbage collector's Gen 0 pressure. So, our alternative to exception is OneOf, which significantly saved time and memory. We can further add Objects to the OneOf, considering the possible return values of the method.
In exceptions, Stack tracing is very expensive, as it propagates stacks, captures method names, stores IL offsets, and inspects frames. Also, the JIT inlining is limited during exceptions. With OneOfI used a struct value type, so Gen 0 utilization is minimized. Neither does it fall for stack trace nor unwind it. Hence, the execution remains linear.
When can I use an exception alternative?
In the following cases, exceptions can be replaced with Result or OneOf in normal application flows.
- Business rule rejection, such as the customers cannot order out-of-stock items. You can return an error in response.
- API validation, where you can simply return 400 with a custom message after figuring out all possible error cases.
- High-throughput paths where you cannot afford an exception mechanism.
- Validation failure, such as invalid email or mobile number input.
- Data not found scenarios where you know either the request data will be available or will not be found. Simply, you can deal with both cases.
When is an exception the optimal choice?
You don't remove fire alarms from a building because they're loud. You just don't pull them every time someone burns toast. We have some situations where exceptions stand out even if they are expensive.
- Exceptions occur when the program falls into an impossible state, such as when a null database connection is used. You cannot proceed anywhere because the connection is not even initialized for some reason.
- For environmental failures, you will opt for exceptions such as timeout failures, disk I/O failures, or database connection losses.
- Programming bugs where your code falls into a dead end, and it cannot handle further. Conditions where your input case exhaust, such as you have order statuses of pending, cancelled, and confirmed, are enumerated with 1,2 and 3, respectively. There is no case apart from that, so you can simply throw an
ArgumentOutOfRangeExceptionor a custom exception in the default case. - If developing a library, use an exception to signal to the user what went wrong and halt normal execution. Here, you cannot force consumers to handle result types.
Conclusion
In high-performance systems, every allocation matters. Exceptions aim to provide a safeguard against anomalous conditions, but they can sometimes be a burden on memory and CPU. I put light on the exception of how much resource they can use for simple operations compared to their counterparts. We explored where it is suitable and where it can be replaced. In short, use exceptions for Unexpected, impossible, and environmental failures. While you can simply use Result/OneOf As an alternative, when conditions are expected, it is useful for business validation, user-driven errors, and high-frequency failures.
Code: https://github.com/elmahio-blog/ExceptionBenchmarks.git
elmah.io: Error logging and Uptime Monitoring for your web apps
This blog post is brought to you by elmah.io. elmah.io is error logging, uptime monitoring, deployment tracking, and service heartbeats for your .NET and JavaScript applications. Stop relying on your users to notify you when something is wrong or dig through hundreds of megabytes of log files spread across servers. With elmah.io, we store all of your log messages, notify you through popular channels like email, Slack, and Microsoft Teams, and help you fix errors fast.
See how we can help you monitor your website for crashes Monitor your website