Introduction
The C# ecosystem has long relied on exceptions for signaling failure. While exceptions are powerful for truly exceptional, unexpected conditions (like a lost network connection or a corrupted file), using them for expected failure modes—such as user input validation or a “not found” result from a database—often leads to code that is less readable, less composable, and prone to runtime errors.
Esox.SharpAndRusty is a production-ready C# library that introduces the Result<T, E> type, a core concept from the Rust programming language and functional programming paradigms, to provide a type-safe and explicit way to handle operations that can either succeed or fail.
This article, the first in a series, will introduce you to the fundamental building blocks of the library: the Result<T, E> type itself and its two most powerful functional composition methods, Map and Bind. By the end, you’ll understand how these tools can significantly improve your C# application’s robustness and code clarity.
💡 The Problem with Traditional Error Handling
Consider a typical C# method that retrieves a user:
C#
public User GetUser(int id)
{
var user = database.FindUser(id);
if (user == null)
throw new NotFoundException($"User {id} not found"); // Failure signaled via exception
return user;
}
// Caller has no indication this method can throw
User user = GetUser(123); // Might throw at runtime!
The potential for failure is not communicated in the method’s signature. A caller must know to wrap the call in a try...catch block, or the program will crash. This is implicit error handling.
✅ The Result<T, E> Solution
The Result<T, E> type explicitly represents two mutually exclusive states:
- Success (
Ok(T)): Contains a value of typeT(the success value). - Failure (
Err(E)): Contains a value of typeE(the error value).
With SharpAndRusty, the same function becomes:
C#
public Result<User, string> GetUser(int id)
{
var user = database.FindUser(id);
if (user == null)
return Result<User, string>.Err($"User {id} not found");
return Result<User, string>.Ok(user);
}
// Failure is explicit in the type signature
Result<User, string> result = GetUser(123);
// The compiler now forces the caller to handle both the Ok and Err cases
The failure state is now part of the type signature. This provides compile-time guarantees about error handling, preventing unexpected crashes and leading to better code flow. Because the library implements Result<T, E> as a readonly struct, it achieves zero overhead and optimal performance by avoiding heap allocations for typical usage.
⛓️ Functional Composition: Map and Bind
The real power of Result<T, E> comes from its functional composition methods, which allow you to chain operations together without needing to check for the success or failure state after every single call. Errors simply propagate automatically.
🗺️ Transforming Success with Map
The Map<T, E, U> method is used to transform the success value (T) into a new success value (U), without altering the error type (E). If the Result is an error, the error is simply passed along, and the transformation function is never executed.
Scenario: You successfully get a user’s age, but you want to transform that integer into a formatted string.
C#
Result<int, string> GetUserAge() => Result<int, string>.Ok(25);
// Transform the success value (int -> string)
var result = GetUserAge()
.Map<int, string, string>(age => $"User is {age} years old");
// Result: Ok("User is 25 years old")
// If it had failed, the error propagates:
Result<int, string> failed = Result<int, string>.Err("User not found");
var mappedFailed = failed.Map<int, string, string>(age => $"User is {age} years old");
// Result: Err("User not found")
The Map method is perfect for transforming values when you know the operation itself cannot fail.
🔗 Chaining Fallible Operations with Bind
The Bind<T, E, U> method, also known as flatMap or andThen, is the key to chaining multiple operations where each one can fail.
Unlike Map, the function passed to Bind must return another Result<U, E>. This allows the chain to stop immediately upon encountering the first error.
Scenario: You need to parse a string, divide it, and then validate that the result is positive. Each step can fail.
C#
// Functions that return a Result:
Result<int, string> ParseInt(string input) { /* ... */ }
Result<int, string> Divide(int numerator, int denominator) { /* ... */ }
Result<int, string> ValidatePositive(int value) { /* ... */ }
// Chain operations - stops at first error:
var result = ParseInt("100")
.Bind(value => Divide(value, 5)) // Ok(20)
.Bind(value => ValidatePositive(value)); // Ok(20)
// Result: Ok(20)
// Failure Scenario:
var failedResult = ParseInt("100")
.Bind(value => Divide(value, 0)) // Err("Division by zero") - Stops here!
.Bind(value => ValidatePositive(value)); // Never executes
// Result: Err("Division by zero")
This chaining capability replaces deep nesting of if statements or complex try...catch blocks, resulting in highly composable and flat code.
Conclusion: 🌟 Why Use Esox.SharpAndRusty?
SharpAndRusty isn’t just a basic implementation; it’s designed to be production-ready and offers significant advantages:
- LINQ Query Syntax: It provides full support for C# LINQ query comprehension (
from,select, etc.), allowing you to write complexBindchains using familiar query syntax, making functional error handling feel native to C#. - Performance: The
readonly structimplementation ensures zero overhead, avoiding the performance pitfall of exception-based control flow. - Comprehensive Tooling: Beyond
MapandBind, it includes methods for safe value extraction (TryGetValue,UnwrapOr), elegant pattern matching (Match), and helpful exception handling helpers (Try,TryAsync) to wrap legacy code. - Asynchronous Support: It features complete async/await integration with methods like
MapAsyncandBindAsync, ensuring it works seamlessly in modern, I/O-bound applications.
The decision to adopt a result-based approach is a decision to adopt explicit error handling and type safety. By using Result<T, E>, you communicate potential failures directly in your type signatures, gain compile-time guarantees, and enable a highly composable style of programming that is easier to test and reason about.
If you are looking to build robust, modern C# applications that avoid NullReferenceException pitfalls and minimize unexpected runtime errors, adopting the Result<T, E> type via Esox.SharpAndRusty is a powerful step forward. It allows you to transform complex, exception-laden logic into a clear, functional pipeline where errors are expected, explicitly handled, and automatically propagated.
Want to learn more? In the next article, we’ll dive into the elegant LINQ Query Syntax and Pattern Matching capabilities that make working with Result<T, E> feel perfectly at home in C#.
API Quick Reference
| Method | Purpose | Signature |
Ok(T value) | Create a successful result | Result<T, E> |
Err(E error) | Create a failed result | Result<T, E> |
Map | Transform success value without changing error type | Func<T, U> |
Bind | Chain fallible operations that return a Result | Func<T, Result<U, E>> |
Match | Handle both success and failure states elegantly | Func<T, R>, Func<E, R> |




