Background
As a developer, whether you intend to or not, you leave an impact on others. That impact-positive or negative-often stems from the code you write, the actions you take, or the words you say. In this case, I was asked why I used the Result Pattern in a recent project, especially given the relatively minimal use of exception handling. This was contrasted with another codebase heavily reliant on exceptions to control application flow.
While many decisions in software development are subjective, I chose to address this topic because it highlights one critical, objective principle. So let’s dive in.
Design objectivity
In C#, specifically (though this may differ in other languages), exceptions are intended to handle unexpected behaviour-not to manage regular application control flow. If you'd prefer to hear it straight from Microsoft, you can read their definition and their best practices. By design, we should avoid using exceptions for normal program logic and reserve them for genuinely unforeseen situations or cases where a meaningful recovery action is necessary.
(Apparently i've not implemented embedding links, so errr enjoy raw links)
https://learn.microsoft.com/en-us/dotnet/standard/exceptions/
https://learn.microsoft.com/en-us/dotnet/standard/exceptions/best-practices-for-exceptions
Why the result pattern?
Let’s start with what the Result Pattern is, rather than why it matters.
The Result Pattern is a design approach that allows operations to be handled in a fully deterministic way. Typically, it represents outcomes explicitly as either a success or a failure, without relying on exceptions being thrown outside the method that implements it. This results in a pure function-one with no side effects-and offers a clear, structured way to represent the outcome of an operation. It enables developers to consistently handle both success and error scenarios.
The key takeaway is that this pattern eliminates side-effect-driven logic. An operation either succeeds or fails-there’s no fallback on exception handling as a control mechanism. This contrasts with many traditional applications where the pattern is “succeed or throw,” meaning exceptions can bubble up from any downstream dependency, leading to ambiguous and scattered error handling.
Ultimately, it’s not about the Result Pattern itself-it’s about avoiding the misuse of exceptions to manage normal control flow. You’re free to use any approach that enforces this principle.
The objective take
You could still argue that all of this is highly opinionated, as with many debates you could file this under philosophical, so lets talk facts. Exceptions are objectively slow.
I used benchmark dotnet to crunch some numbers.
Test Setup
public class Program
{
public static void Main(string[] args)
{
BenchmarkRunner.Run<ExceptionsVsResultPattern>();
}
}
[SimpleJob(RuntimeMoniker.HostProcess)]
public class ExceptionsVsResultPattern
{
private readonly CustomerRepository _customerRepository = new CustomerRepository();
[Benchmark]
public void Exceptions() => _ = _customerRepository.AddCustomerWithException("");
[Benchmark]
public void ResultPattern() => _ = _customerRepository.AddCustomerWithResultPattern("");
}
Result Pattern Example
public class Result<T>
{
public T Value { get; }
public string? Error { get; }
public bool IsSuccess => Error == null;
private Result(T value, string error)
{
Value = value;
Error = error;
}
public static Result<T> Success(T value) => new Result<T>(value, null);
public static Result<T> Failure(string error) => new Result<T>(default, error);
}
Test Classes
public class CustomerRepository
{
public Customer? AddCustomerWithException(string name)
{
if (string.IsNullOrEmpty(name))
{
throw new ArgumentException("Name cannot be null or empty", nameof(name));
}
try
{
// Futher processing
return new Customer { Name = name };
} catch (Exception e)
{
//Maybe even a throw instead of null return?
return null;
}
}
public Result<Customer> AddCustomerWithResultPattern(string name)
{
try
{
if (string.IsNullOrEmpty(name))
{
return Result<Customer>.Failure("Name cannot be null or empty");
}
//Further processing
return Result<Customer>.Success(new Customer { Name = name });
}
catch (Exception ex)
{
//Handle your unexpected things here and keep it encapsulated.
return Result<Customer>.Failure(ex.Message);
}
}
}
The results
BenchmarkDotNet v0.14.0, Windows 11 (10.0.26100.3915)
AMD Ryzen 9 7900X, 1 CPU, 24 logical and 12 physical cores
.NET SDK 9.0.102
[Host] : .NET 8.0.13 (8.0.1325.6609), X64 RyuJIT AVX-512F+CD+BW+DQ+VL
| Method | Mean | Error | StdDev | |-------------- |-------------:|-----------:|-----------:| | Exceptions | 2,818.817 ns | 21.2729 ns | 17.7638 ns | | ResultPattern | 2.968 ns | 0.0760 ns | 0.0747 ns |
Analysis
As you can see, using exceptions for regular control flow is approximately 950% slower than avoiding them. You might not care in smaller applications, but at enterprise scale, this performance difference becomes significantly more impactful. It's definitely something worth keeping in mind.
In Summary
There are three big reasons to avoid using exceptions for normal application control flow;
Performance Overhead
Throwing exceptions is computationally expensive. When an exception is thrown, the .NET runtime must unwind the stack and create an exception object, which is resource-intensive. See the results above.
Code Readability and Maintainability
Relying on exceptions for regular control flow can make code harder to read and maintain. It obscures the normal execution path and can lead to complex try-catch blocks scattered throughout the codebase. (the original prompt this article)
Unpredictable Error Handling
Exceptions can be caught at various levels in the call stack, making it difficult to predict where and how errors are handled. This unpredictability can lead to bugs and makes the codebase harder to debug and test.
Try the following three things instead;
Use exceptions sparingly
Reserve exceptions for truly unexpected errors that cannot be handled gracefully or big challenging scenarios which you want to err on the side of caution with.
Adopt the Result Pattern
For operations where failure is a valid outcome, use the Result pattern to handle errors explicitly.
Consistent Error Handling
Implement a consistent error-handling strategy across your codebase to improve readability and maintainability.