RustlikeValues.Result
2.0.1
dotnet add package RustlikeValues.Result --version 2.0.1
NuGet\Install-Package RustlikeValues.Result -Version 2.0.1
<PackageReference Include="RustlikeValues.Result" Version="2.0.1" />
<PackageVersion Include="RustlikeValues.Result" Version="2.0.1" />
<PackageReference Include="RustlikeValues.Result" />
paket add RustlikeValues.Result --version 2.0.1
#r "nuget: RustlikeValues.Result, 2.0.1"
#:package RustlikeValues.Result@2.0.1
#addin nuget:?package=RustlikeValues.Result&version=2.0.1
#tool nuget:?package=RustlikeValues.Result&version=2.0.1
RustLike Result<T, E> for C#
A complete implementation of Rust's Result type for C#, providing explicit error handling as values instead of exceptions.
Installation
dotnet add package RustLikeValues.Result
Overview
Result<T, E> represents either success (Ok) with a value of type T, or failure (Err) with an error of type E. This enables explicit error handling, making error cases visible in method signatures and enforcing their handling.
Key Features
- Explicit Error Handling: Errors are values, not hidden exceptions
- Smart Implicit Conversions: Automatic Ok/Err inference when types differ
- Railway-Oriented Programming: Chain operations with automatic error propagation
- Pattern Matching: Full support for C# pattern matching
- Zero Exceptions: Replace try-catch with functional error handling
- Type Safety: Compile-time guarantees about error handling
Quick Start
using RustLikeValues.RustLikeResult;
// Implicit conversions when T and E are different types
Result<int, string> Divide(int a, int b)
{
if (b == 0)
return "Cannot divide by zero"; // Implicit Err
return a / b; // Implicit Ok
}
// Pattern matching
var message = result.Match(
Ok: value => $"Success: {value}",
Err: error => $"Failed: {error}"
);
// Railway-oriented programming
var final = GetUser(id)
.AndThen(user => ValidateUser(user))
.Map(user => user.Email)
.MapErr(err => $"Operation failed: {err}")
.UnwrapOr("no-email@default.com");
Implicit Conversion Rules
The library provides smart implicit conversions:
When T and E are Different Types
Result<string, Exception> ProcessName(string name)
{
if (string.IsNullOrWhiteSpace(name))
return new ArgumentException("Name is empty"); // Implicit Err
return name.ToUpper(); // Implicit Ok
}
Result<User, string> GetUser(int id)
{
if (id < 0)
return "Invalid user ID"; // Implicit Err
return new User { Id = id }; // Implicit Ok
}
When T and E are the Same Type
When types are ambiguous, you need to be explicit:
Result<string, string> Transform(string input)
{
if (string.IsNullOrEmpty(input))
return Result.Err("Input was empty"); // Explicit Err
return input.ToUpper(); // Implicit Ok (default for success)
}
Core API
Creation
// Implicit conversions (when T != E)
Result<int, string> r1 = 42; // Ok(42)
Result<int, string> r2 = "error"; // Err("error")
// Explicit creation (always works)
var r3 = Result.Ok(42); // OkValue<int>
var r4 = Result.Err("error"); // ErrValue<string>
var r5 = Result<int, string>.Ok(42); // Result<int, string>
var r6 = Result<int, string>.Err("error"); // Result<int, string>
// Type helpers for ambiguous cases
var r7 = Result.Success<int, string>(42); // Ok
var r8 = Result.Failure<int, string>("err"); // Err
Checking State
if (result.IsOk)
Console.WriteLine("Success!");
if (result.IsErr)
HandleError(result.UnwrapErr());
// Implicit bool conversion
if (result) // true if Ok
Process(result.Unwrap());
Extracting Values
// Unwrap - throws if wrong variant
var value = result.Unwrap(); // Throws if Err
var error = result.UnwrapErr(); // Throws if Ok
// Safe extraction
var value1 = result.UnwrapOr(defaultValue);
var value2 = result.UnwrapOrElse(err => ComputeDefault(err));
Transforming
// Map - transform Ok value
Result<string, Error> upperName = result.Map(name => name.ToUpper());
// MapErr - transform Err value
Result<int, string> withContext = result.MapErr(err => $"Context: {err}");
// AndThen - chain operations that return Result
Result<Order, Error> order = GetUser(id)
.AndThen(user => GetLatestOrder(user.Id))
.AndThen(order => ValidateOrder(order));
Pattern Matching
// Match with return value
string message = result.Match(
Ok: value => $"Success: {value}",
Err: error => $"Error: {error}"
);
// Match with side effects
result.Match(
Ok: value => Console.WriteLine($"Got: {value}"),
Err: error => logger.LogError(error)
);
// Deconstruction
var (isOk, value, error) = result;
if (isOk)
Process(value);
else
HandleError(error);
Common Patterns
Error Context/Wrapping
public Result<Config, string> LoadConfig(string path)
{
return ReadFile(path)
.MapErr(err => $"Failed to read config from {path}: {err}")
.AndThen(content => ParseJson<Config>(content))
.MapErr(err => $"Invalid config format: {err}");
}
Early Returns with Errors
public Result<Invoice, Error> ProcessOrder(Order order)
{
// Validate order
var validation = ValidateOrder(order);
if (validation.IsErr)
return validation.UnwrapErr(); // Early return with error
// Check inventory
var inventory = CheckInventory(order.Items);
if (inventory.IsErr)
return inventory.UnwrapErr();
// Create invoice
return CreateInvoice(order);
}
Collecting Results
public Result<List<User>, Error> GetAllUsers(List<int> ids)
{
var results = ids.Select(id => GetUser(id)).ToList();
// Check if any failed
var firstError = results.FirstOrDefault(r => r.IsErr);
if (firstError.IsErr)
return firstError.UnwrapErr();
// All succeeded - collect values
var users = results.Select(r => r.Unwrap()).ToList();
return users;
}
Try-Catch Replacement
// Instead of try-catch
public Result<Data, Exception> ParseData(string json)
{
try
{
var data = JsonSerializer.Deserialize<Data>(json);
return data ?? Result.Err(new Exception("Null result"));
}
catch (Exception ex)
{
return ex; // Implicit Err
}
}
// Or use Try extension
var result = Try.Execute(() => JsonSerializer.Deserialize<Data>(json));
Async Error Handling
public async Task<Result<User, ApiError>> GetUserAsync(int id)
{
var response = await httpClient.GetAsync($"/users/{id}");
if (!response.IsSuccessStatusCode)
return new ApiError(response.StatusCode); // Implicit Err
var json = await response.Content.ReadAsStringAsync();
var user = JsonSerializer.Deserialize<User>(json);
return user ?? Result.Err(new ApiError("Empty response"));
}
// Async chaining
var result = await GetUserAsync(id)
.MapAsync(async user => await EnrichUserAsync(user))
.AndThenAsync(async user => await ValidateAsync(user));
Advanced Patterns
Railway-Oriented Programming
public Result<ProcessedOrder, OrderError> ProcessOrder(OrderRequest request)
{
return ValidateRequest(request)
.AndThen(CreateOrder)
.AndThen(CheckInventory)
.AndThen(CalculatePricing)
.AndThen(ApplyDiscounts)
.AndThen(ChargePayment)
.AndThen(UpdateInventory)
.Map(order => new ProcessedOrder(order))
.MapErr(error => new OrderError($"Order processing failed: {error}"));
}
Custom Error Types
public enum ValidationError
{
MissingField,
InvalidFormat,
OutOfRange
}
public Result<Email, ValidationError> ParseEmail(string input)
{
if (string.IsNullOrWhiteSpace(input))
return ValidationError.MissingField;
if (!input.Contains("@"))
return ValidationError.InvalidFormat;
return new Email(input); // Implicit Ok
}
Combining Results
public Result<Summary, Error> GenerateSummary(int userId)
{
var userResult = GetUser(userId);
var ordersResult = GetUserOrders(userId);
var prefsResult = GetUserPreferences(userId);
// If any failed, return first error
if (userResult.IsErr) return userResult.UnwrapErr();
if (ordersResult.IsErr) return ordersResult.UnwrapErr();
if (prefsResult.IsErr) return prefsResult.UnwrapErr();
// All succeeded
return new Summary(
userResult.Unwrap(),
ordersResult.Unwrap(),
prefsResult.Unwrap()
);
}
Best Practices
Use descriptive error types
// ❌ Avoid generic errors Result<User, string> GetUser(int id); // ✅ Prefer specific error types Result<User, UserNotFoundError> GetUser(int id);Let implicit conversions work for you
Result<Data, Exception> LoadData(string path) { if (!File.Exists(path)) return new FileNotFoundException(path); // Implicit Err var content = File.ReadAllText(path); return ParseData(content); // Implicit Ok }Chain operations instead of nested checks
// ❌ Avoid var r1 = Step1(); if (r1.IsErr) return r1.UnwrapErr(); var r2 = Step2(r1.Unwrap()); if (r2.IsErr) return r2.UnwrapErr(); return Step3(r2.Unwrap()); // ✅ Prefer return Step1() .AndThen(Step2) .AndThen(Step3);Use Match for exhaustive handling
return result.Match( Ok: user => RenderUserProfile(user), Err: error => RenderErrorPage(error) );
Comparison with Exceptions
| Aspect | Result<T, E> | Exceptions |
|---|---|---|
| Visibility | Errors in method signature | Hidden in implementation |
| Performance | No stack unwinding | Stack unwinding overhead |
| Composability | Chainable operations | Try-catch nesting |
| Type safety | Compile-time checking | Runtime surprises |
| Control flow | Explicit with returns | Implicit with throws |
Performance
- Value type (struct) implementation
- No heap allocations
- No exception overhead
- Inlined operations
- Suitable for hot paths
Quick Reference
| Method | Description | Example |
|---|---|---|
Ok(value) |
Create Ok result | Result.Ok(42) |
Err(error) |
Create Err result | Result.Err("failed") |
IsOk/IsErr |
Check variant | if (result.IsOk) |
Map(func) |
Transform Ok value | r.Map(x => x * 2) |
MapErr(func) |
Transform Err value | r.MapErr(e => $"Error: {e}") |
AndThen(func) |
Chain Result operations | r.AndThen(Validate) |
Match(ok, err) |
Pattern match | r.Match(v => v, e => 0) |
Unwrap() |
Get Ok value (throws if Err) | result.Unwrap() |
UnwrapOr(def) |
Get Ok or default | result.UnwrapOr(0) |
License
MIT License - see LICENSE file for details
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net9.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.