Introduction
In the world of C# we have been trained to treat errors as occurring under exceptional circumstances, or exceptions if you will. We throw exceptions like stones, hoping someone further up the in the call chain is waiting with a catch to grap and handle them.
But as applications grow in complex, this ‘throw-and-hope’ architecture can become a primary source of bug, performance bottlenecks, and unreadable “Pyramids of Doom”.
So, it is time to stop throwing stone, it is time to declare errors a first class citizen.
The Hidden Cost of Exceptions
We often hear exceptions are expensive, but we rarely talk about why they cost us so much.
The Performance Tax
When an exception is thrown, the runtime must capture a snapshot of the call stack. This is a CPU-intensive operation. In high-throughput applications, for example a web API processing thousands of requests, using exceptions for expected failurs (like a user not being found or a validation error) can degrade performance by orders of magnitude.
The Readability Tax
Exception tend to break the golden path of yoru instead. Instead of a linear flow, your logic jumps from a try block to a catch block somewhere else. This looks like ‘goto’ style of programming and it makes it incredibly difficult to follow the business logic, as the error handling is decouple from the operation which caused it.
The “Lying” Signature
Consider this common method signature:
public User GetUser(int id);
Basically, this signature is lie, or at least it is economic with the truth. Look at it with a fresh look you might think that this method returns a User based on the provided id. But the truth is this:
- It might return
nullif the user does not exist. - Also it might throw an
SqlExceptionif there is a problem with the database. - And it could also throw a
NotFoundExceptionfor several reasons.
As a caller, you have no way of knowing how to safely use this method without checking the implementation details or the documentation. In this particular case the type system is failing to protect you..
Enter Result<T,E>: Making the Failure Explicit
The Result pattern, popularized by languages like Rust and implemented in .NET in the Esox.SharpAndRusty package changes the contract. The method from our previous example would been written like this:
public Result<User, Error> GetUser(int id);
Now the signature says: “I will return a User or an Error. You cannot have the User until you acknowledge the possibility of the Error”
By using Result<T,E> failure is not something that “happens” to your code; it is a pieces of data that you move through your system, just like a success value.
Side-by-Side: The “Stone” vs. “Result”
To demonstrate this even further, let’s have a look at how the code actually changes. Image if you will a process where we fetch a user and then calculate their premium status.
The Traditional way with Exceptions
try
{
var user = service.GetUser(123); // Might throw or return null
var status = calculator.GetStatus(user);
Console.WriteLine($"Status: {status}");
}
catch (NotFoundException ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine("Something went wrong.");
}
The SharpAndRusty Way using Match()
Using Result<T,E> we treat the outcome as a fork in the road. The Match() method forces you to handle both paths immediately:
var result = service.GetUser(123)
.Bind(user => calculator.GetStatus(user));
result.Match(
success: status => Console.WriteLine($"Status: {status}"),
failure: error => Console.WriteLine($"Error: {error.Message}")
);
Why does the result pattern win here?
- No null checks:You cannot accidentally acces user if it does not exist.
- Chaining: Did you notice the
Bind()method? IfGetUser()fails,GetStatus()is never even called, the error just flows to the end. - Exhaustiveness: You are visually and logically prompted to handle the failure case.
Conclusion
Let’s be clear: Exceptions should be reserved for things that truly shouldn’t happen, for example a lost connection to a server, or a corrupted disk. For business logic failures, use a a patten that respects your type system.
By switching to Esox.SharpAndRusty you aren not just writing cleaner code; you care writing honest code. You are in fact building applications where the compilere helps you handle errors instead of letting them crash your production environment.




