ZeroResult provides allocation-free result monads for .NET 8+ with full async support and fluent APIs. Perfect for high-performance applications where traditional exception handling is too costly.
$ dotnet add package ZeroResultZeroResult provides allocation-free result monads for .NET 8+ with full async support and fluent APIs. Perfect for high-performance applications where traditional exception handling is too costly.
✅ Zero Allocations in Happy Path
StackResult uses ref struct to eliminate heap allocations completely when operations succeed
✅ Modern C# 12+ Integration
Leverages the latest language features for optimal performance and expressiveness
✅ Seamless Async Support
Full async/await compatibility with ValueTask-based operations and fluent chaining
StackResult (allocation-free ref struct) and Result (flexible readonly struct)Map, Bind, Match, Ensure, TapIError, SingleError, and MultiErrordotnet add package ZeroResult
Result<int, SingleError> Calculate(int input)
{
return input != 0
? 100 / input
: new SingleError("Division by zero");
}
var result = Calculate(10);
result.Match(
onSuccess: value => Console.WriteLine($"Result: {value}"),
onFailure: error => Console.WriteLine($"Error: {error.Message}")
);
Result<int, SingleError> result = Result.Success<int, SingleError>(5)
.Map(x => x * 2) // Transforms value if successful
.Bind(x => x < 100
? x * 3
: Result.Failure<int, SingleError>(new SingleError("Too large")))
.Ensure(x => x % 2 == 0, () => new SingleError("Must be even"));
async ValueTask<Result<string, SingleError>> ProcessDataAsync(int id)
{
return await FetchDataAsync(id)
.MapAsync(async data => await TransformAsync(data))
.BindAsync(async transformed => await ValidateAsync(transformed))
.OnSuccessAsync(async result => await LogSuccessAsync(result))
.OnFailureAsync(async error => await LogErrorAsync(error));
}
Result<User, ValidationError> ValidateUser(User user)
{
return Result.Success<User, ValidationError>(user)
.Ensure(u => u.Age >= 18, () => new ValidationError("Underage"))
.Ensure(u => !string.IsNullOrEmpty(u.Email), () => new ValidationError("Invalid email"));
}
async ValueTask<Result<Report, BusinessError>> GenerateReportAsync(int userId)
{
return await GetUser(userId)
.Map(user => new ReportRequest(user))
.BindAsync(async request => await ValidateRequestAsync(request))
.MapAsync(async validRequest => await GenerateReportAsync(validRequest));
}
// Traditional approach (expensive exceptions)
try {
var value = RiskyOperation();
Process(value);
}
catch (Exception ex) {
HandleError(ex);
}
// ZeroResult approach (explicit error handling)
var result = SafeOperation();
result.Match(
onSuccess: Process,
onFailure: HandleError
);
Result<Order, OrderError> orderResult = ProcessOrder(orderId);
string message = orderResult.Match(
onSuccess: order => $"Order {order.Id} processed",
onFailure: error => $"Failed: {error.Message}"
);
// Async version
string asyncMessage = await orderResult.MatchAsync(
onSuccess: async order => await FormatOrderAsync(order),
onFailure: async error => await FormatErrorAsync(error)
);
await GetUserAsync(userId)
.TapAsync(async user => await AuditAccessAsync(user))
.MapAsync(user => user.Profile)
.Tap(profile => CacheProfile(profile));
ZeroResult's MultiError provides sophisticated error aggregation for complex validation scenarios, batch processing, and cases where multiple failures need to be reported simultaneously.
var errors = new IError[] {
new SingleError("Invalid email format", "VAL-001"),
new SingleError("Password must be 8+ characters", "SEC-002"),
new SingleError("Username already exists", "USER-003")
};
Result<Unit, MultiError> validationResult = new MultiError(errors);
Result<User, MultiError> ValidateUser(UserInput input)
{
var builder = MultiError.CreateBuilder();
if (string.IsNullOrEmpty(input.Email))
builder.Add(new SingleError("Email required", "REQ-001"));
if (input.Password.Length < 8)
builder.Add(new SingleError("Password too short", "SEC-001"));
if (input.Age < 18)
builder.Add(new SingleError("Must be 18+", "AGE-001"));
return builder.Count > 0
? Result.Failure<User, MultiError>(builder.Build())
: MapToUser(input);
}
async ValueTask<Result<BatchReport, MultiError>> ProcessBatchAsync(int[] ids)
{
var builder = MultiError.CreateBuilder();
var successes = new List<ItemResult>();
foreach (var id in ids)
{
var result = await ProcessItemAsync(id);
result.Match(
onSuccess: successes.Add,
onFailure: builder.Add
);
}
return builder.Count > 0
? Result.Failure<BatchReport, MultiError>(builder.Build())
: new BatchReport(successes);
}
var addressResult = ValidateAddress(order.Address);
var paymentResult = ValidatePayment(order.PaymentMethod);
if (addressResult.IsFailure || paymentResult.IsFailure)
{
var mergedErrors = MultiError.Merge(
addressResult.IsFailure ? addressResult.Error : MultiError.Empty,
paymentResult.IsFailure ? paymentResult.Error : MultiError.Empty
);
return Result.Failure<OrderConfirmation, MultiError>(mergedErrors);
}
Result<LoanApplication, MultiError> ValidateApplication(LoanApplication app)
{
var builder = MultiError.CreateBuilder();
// Financial validation
if (app.Income < app.MonthlyPayment * 3)
builder.Add(new SingleError("Income insufficient", "FIN-001"));
// Document validation
if (app.RequiredDocuments.Count < 3)
builder.Add(new SingleError("Missing documents", "DOC-002"));
// Business rules
if (app.Age < 21)
builder.Add(new SingleError("Minimum age not met", "AGE-003"));
// Custom validation method
ValidateCreditHistory(app.CreditScore, builder);
return builder.Count > 0
? Result.Failure<LoanApplication, MultiError>(builder.Build())
: app;
}
void ValidateCreditHistory(int score, MultiErrorBuilder builder)
{
if (score < 650)
builder.Add(new SingleError("Poor credit history", "CRD-004"));
}
MultiError automatically generates structured error messages:
var error = new MultiError(new IError[] {
new SingleError("Invalid email format", "VAL-001"),
new SingleError("Password too short", "SEC-002"),
new SingleError("Terms not accepted", "REQ-003")
});
Console.WriteLine(error.Message);
/* Output:
Multiple errors occurred (3):
- Invalid email format (Code: VAL-001)
- Password too short (Code: SEC-002)
- Terms not accepted (Code: REQ-003)
*/
✔ Complex form validations
✔ Batch processing pipelines
✔ Distributed system integrations
✔ Business rule engines
✔ Data migration tools
Pro Tip: Combine MultiError with StackResult for allocation-free validation in hot paths:
StackResult<Transaction, MultiError> ValidateTransaction(Transaction tx)
{
var builder = MultiError.CreateBuilder();
// ... validation logic
return builder.Count > 0
? StackResult.Failure<Transaction, MultiError>(builder.Build())
: tx;
}
ZeroResult dramatically outperforms traditional exception handling, especially in deep call stacks and error scenarios. Benchmarks were run on .NET 9.0.6 using BenchmarkDotNet v0.15.1, with results collected from two major platforms:
| Scenario | Approach | Mean Time | Allocated | vs Try/Catch |
|---|---|---|---|---|
| All Failures | Try/Catch | 2,521 μs | 427 KB | 1.00x |
| StackResult (Imperative) | 14.8 μs | 47 KB | 170x faster | |
| Result (Fluent) | 24.7 μs | 175 KB | 100x faster | |
| 75% Success Rate | Try/Catch | 631 μs | 103 KB | 1.00x |
| StackResult (Imperative) | 18.4 μs | 11 KB | 34x faster | |
| Result (Fluent) | 27.8 μs | 139 KB | 23x faster |
| Approach | Call Depth | Mean Time | Memory | Outcome |
|---|---|---|---|---|
| Exceptions | 20 | 111 ms | 15.7 MB | Works |
| 200 | - | - | Stack Overflow | |
| ZeroResult (MultiError) | 20 | 2.6 ms | 6.9 MB | 43x faster |
| 200 | 25 ms | 65 MB | Still works |
| Scenario | Approach | Allocations | Reduction |
|---|---|---|---|
| Async Operations (100% errors) | Try/Catch | 2.77 MB | - |
| ZeroResult | 625 KB | 77% less | |
| Method Chaining (100% errors) | Try/Catch | 427 KB | - |
| ZeroResult | 47 KB | 89% less |
If you're interested in the full benchmark results or want to explore detailed metrics for specific scenarios, check out the benchmark results folder.
ZeroResult - Where performance meets reliability in .NET error handling. Contribute on GitHub!