Stop Throwing Exceptions: Bringing Rust-Style Error Handling to C#

Introduction

As C# developers, we have all been there. You are calling a method like GetUser(id), and you wonder: does it return null if the user is not found? Or does it throw a NotFoundException? Or perhaps even and InvalidOperationException?

The signature public User GetUser(int id) is confusing. It implies we will retrieve a user, but often we are faced with an exception instead.

Exceptions are expensive, implicit, and often break the natural flow of control. While working with Rust, a language I really love, I fell in love with the Result<T,E> pattern, which is away to treat errors as values, not catastrophes. I wanted that same type safety and functional elegance in my .NET projects.

That is why Esox.SharpAndRusty brings the power of Rust’s Result<> type to C#. (With some some limitations, as C# enums are at the moment less powerful than Rust’s)

Mind that this package is in its first version, so if you find errors, or want extra features, please let me know.

The package is available on Nuget under the same name. The source code is available at https://github.com/snoekiede/Esox.SharpAndRusty

What is Esox.SharpAndRusty?

Esox.SharpAndRusty is a library designed to make error handling explicit, type-safe and performant. Instead of hoping a method succeeds, the return signature tells you exactly what to expect:

// The old way: Might throw, might return null?
public User GetUser(int id) { ... }

// The SharpAndRusty way: Explicit success (User) or failure (string)
public Result<User, string> GetUser(int id) { ... }

It is implemented as a readonly struct for zero overhead and designed to be familiar to developers to developers coming from functional programming or Rust.

Why Change How We Handle Errors?

Type-Safe Control Flow.

The biggest benefit is honesty in your type signatures. You can explicitly represent success and failure states. This eliminates the guessing game of guessing the exception.

Functional Composition

This is where the library shines. You can chain operations together. If an operation fails, the chain stop executing automatically, passing the error down the line.

Here is an example of chaining parsing, division and validation with a single try-catch block:

// Chain operations - stops at the first error automatically
var result = ParseInt("100")
    .Bind(value => Divide(value, 5))
    .Bind(value => ValidatePositive(value));
    
// If ParseInt fails, Divide and ValidatePositive never run.
// The error simply propagates to the end.

Elegant Pattern Matching

Say goodbye to nested if statements checking for nulls. Use the Match() method to handle both outcomes cleanly:

var message = success.Match(
    success: value => $"Got value: {value}",
    failure: error => $"Got error: {error}"
);

An efficient implementation

The library targets .NET and C# 14 and includes features essential for production environments:

  • Zero Overhead: It uses a readonly struct to minimize memory allocation.
  • Equality Support: Full implementation of IEquatable<T>, ==, and != operators.
  • Async Support: Built-in TryAsync() helpers for wrapping Task-based operations.
  • Safe Extraction: Methods like UnwrapOr() and UnwrapOrElse() prevent crashes when accessing values.

Real-World Example: Order Processing

Here is how Esox.SharpAndRusty cleans up a complex workflow like processing an order. Note how we validate, process payments, and send emails in a single, readable chain. Side effects (like logging for example) are handled via the Inspect() method, without breaking the flow:

public async Task<Result<OrderConfirmation, string>> ProcessOrderAsync(OrderRequest request)
{
    return await ValidateOrder(request)
        .Bind(order => ProcessPayment(order))
        // Log success without changing the result
        .Inspect(payment => Logger.Info($"Payment processed: {payment.TransactionId}")) 
        .Bind(payment => CreateOrder(payment))
        .Bind(async order => await SendConfirmationEmail(order))
        // Log errors if they happen anywhere in the chain
        .InspectErr(error => Logger.Error($"Order processing failed: {error}"))
        .OrElse(error => 
        {
            // Graceful fallback
            return CreatePendingOrder(request, error);
        });
}

Conclusion

The journey from relying on implicit, costly exceptions to embracing explicit error handling marks a significant step forward for C# development. The ambiguous public User GetUser(int id) signature, which leaves developers guessing between a null return or a runtime crash, can be entirely eliminated.

Esox.SharpAndRusty effectively addresses this by porting the robust, functional elegance of Rust’s Result<T, E> pattern to the .NET ecosystem. This library provides type-safe control flow, allowing methods to honestly declare both success (User) and failure (string) outcomes directly in their signature. Furthermore, its lightweight readonly struct implementation ensures zero overhead, and its fluent API enables powerful functional composition for chaining complex operations with automatic error propagation.

To bring this enhanced clarity and reliability to your projects, Esox.SharpAndRusty is readily available as a NuGet package, allowing C# developers to adopt a more performant and transparent approach to error management today.

The Code Nomad
The Code Nomad
Articles: 167

Leave a Reply

Your email address will not be published. Required fields are marked *