Result
The Result type represents the outcome of an operation that can either succeed or fail with a failure object. It embodies
railway-oriented programming, providing a type-safe alternative to exceptions for control flow.
Table of Contents
Introduction
Use Result when an operation can fail and you want that possibility to be explicit in the type system instead of
hidden in exception-driven control flow.
Core Concepts
Resultmodels success/failure without a value.Result<TValue>models success/failure with a success value.- Failures are typed objects that carry semantics and can be matched explicitly.
- Pipelines compose through
Bind,Map,Ensure,Recover, andTap.
Simple Example
Result<int> ParsePositive(string input) =>
int.TryParse(input, out var value)
? value > 0
? Result.Success(value)
: Result.FailValidation<int>("Value must be positive")
: Result.FailValidation<int>("Input is not a number");
var result = ParsePositive("42")
.Map(value => value * 2)
.Ensure(value => value < 100, _ => new ValidationFailure("Value must be less than 100"));
API Reference
The full API surface has been moved to a dedicated reference page so this guide can stay focused on concepts and usage patterns.
Advanced Topics
Metadata
Results support attaching metadata for contextual information:
// Add single key-value pair
var result = Result.Success()
.WithMetadata("RequestId", "12345");
// Add multiple pairs
var result = Result.Success()
.WithMetadata(
("RequestId", "12345"),
("UserId", "user-123")
);
// Merge metadata
var result = Result.Success()
.WithMetadata(existingMetadata);
// Use builder
var result = Result.Success()
.WithMetadata(builder => {
builder.Add("RequestId", "12345");
builder.Add("Timestamp", DateTime.UtcNow);
});
// Metadata is preserved through transformations
var result = GetData()
.WithMetadata("Operation", "Fetch")
.Bind(() => ProcessData()) // Metadata flows through
.Map(x => x.ToString());
LINQ Support
Results support LINQ query syntax:
// Select (Map)
var result = from user in GetUser()
select user.Name;
// SelectMany (Bind)
var result = from userId in GetUserId()
from user in FetchUser(userId)
select user;
// Where (filter with validation)
var result = from user in GetUser()
where user.Age >= 18
select user;
// Fails with ValidationFailure if predicate is false
// Complex queries
var result = from order in GetOrder()
from customer in GetCustomer(order.CustomerId)
where customer.IsActive
from address in GetAddress(customer.AddressId)
select new OrderDetails(order, customer, address);
Best Practices
1. Use Railway-Oriented Programming
Chain operations and let failures propagate automatically:
// Good
var result = GetUserId()
.Bind(id => FetchUser(id))
.Map(user => user.Email)
.Ensure(email => email.Contains("@"),
_ => new ValidationFailure("Invalid email"));
// Avoid manually checking each step
var userIdResult = GetUserId();
if (userIdResult.IsFailure) return userIdResult.ToResult();
// ...
2. Prefer Specific Failure Types
Use specialized failure factories for better semantics:
// Good
return Result.FailNotFound("User", userId);
return Result.FailValidation("Email is required");
// Less clear
return Result.Failure("User not found");
3. Use Metadata for Context
Attach contextual information without polluting the result:
return GetUser(id)
.WithMetadata("UserId", id)
.WithMetadata("Timestamp", DateTime.UtcNow);
4. Handle Failures at Boundaries
Keep most code using Result, convert to exceptions only at system boundaries:
// In API controller
public IActionResult GetUser(string id)
{
return GetUserById(id)
.Match(
success: user => Ok(user),
failure: failure => failure switch
{
NotFoundFailure => NotFound(failure.Message),
ValidationFailure => BadRequest(failure.Message),
_ => StatusCode(500, failure.Message)
}
);
}
5. Leverage LINQ for Readability
Use LINQ syntax for complex chains:
var result =
from order in GetOrder(orderId)
from customer in GetCustomer(order.CustomerId)
from payment in ProcessPayment(order.Total)
select new { order, customer, payment };
6. Use Tap for Side Effects
Don't break the chain for logging or other side effects:
var result = GetUser(id)
.Tap(user => _logger.LogInformation("User loaded: {Id}", user.Id))
.Map(user => user.ToDto())
.TapFailure(failure => _logger.LogError("Failed: {Error}", failure.Message));
7. Compensate for Transactional Rollbacks
Use Compensate for saga patterns or compensating transactions:
var result = ReserveInventory(productId, quantity)
.Bind(() => ChargePayment(amount))
.Compensate(failure => ReleaseInventory(productId, quantity));
8. Combine for Parallel Operations
Use Combine when validating multiple independent results:
var results = new[]
{
ValidateName(input.Name),
ValidateEmail(input.Email),
ValidateAge(input.Age)
};
return results.Combine(); // Returns all validation errors if any fail
9. Type Safety with Constraints
TValue must be notnull - design your domain types accordingly:
// Good
Result<User> GetUser(string id);
Result<IEnumerable<Order>> GetOrders();
// Not allowed
Result<string?> GetNullableValue(); // Compile error
10. Implicit Conversions
Leverage implicit conversions for cleaner code:
public Result<User> CreateUser(string name)
{
if (string.IsNullOrEmpty(name))
return new ValidationFailure("Name required"); // Implicit conversion
var user = new User(name);
return user; // Implicit conversion
}