Result
The Result monad represents the outcome of an operation that can either succeed or fail. It provides type-safe handling of both success and error states without relying on exceptions for control flow.
Purpose
Section titled “Purpose”Result enables explicit error handling by encoding success and failure as types. This makes error cases visible in the type system and forces consumers to handle both paths explicitly.
Producer Perspective
Section titled “Producer Perspective”Return Result to indicate operation outcomes:
// Simple Resultpublic class OrderProcessor{ public Result ValidateOrder(Order order) { if (order.Items.Count == 0) return Result.Failed();
if (order.TotalAmount <= 0) return Result.Failed();
return Result.Success(); }}
// Result with typed errorpublic record ValidationError(string Field, string Message);
public class OrderValidator{ public Result<ValidationError> Validate(Order order) { if (order.Items.Count == 0) return Result.Failed(new ValidationError("Items", "Order must have at least one item"));
if (order.TotalAmount <= 0) return Result.Failed(new ValidationError("TotalAmount", "Total must be greater than zero"));
return Result.Success<ValidationError>(); }}
// Result with both result and error typespublic record OrderConfirmation(OrderId Id, DateTime ConfirmedAt);public record OrderError(string Code, string Description);
public class OrderService{ public Result<OrderConfirmation, OrderError> ConfirmOrder(OrderId orderId) { var order = _repository.FindById(orderId); if (order is null) return Result.Failed<OrderConfirmation, OrderError>( new OrderError("NOT_FOUND", "Order not found"));
if (order.Status != OrderStatus.Pending) return new OrderError("INVALID_STATE", $"Cannot confirm order in {order.Status} state");
order.Status = OrderStatus.Confirmed; _repository.Save(order);
// Implicit conversion from TResult to Result<TResult, TError> return new OrderConfirmation(orderId, DateTime.UtcNow); }}Consumer Perspective
Section titled “Consumer Perspective”Handle success and failure paths explicitly:
public class OrderController{ readonly OrderService _orderService; readonly OrderValidator _validator;
public void ProcessOrder(Order order) { // Simple Result var validationResult = _validator.ValidateOrder(order); if (!validationResult.IsSuccess) { Console.WriteLine("Order validation failed"); return; }
Console.WriteLine("Order is valid"); }
public void ValidateAndDisplay(Order order) { // Result<TError> var result = _validator.Validate(order);
if (result.IsSuccess) { Console.WriteLine("Validation passed"); } else if (result.TryGetError(out var error)) { Console.WriteLine($"Validation failed: {error.Field} - {error.Message}"); } }
public void ConfirmOrder(OrderId orderId) { // Result<TResult, TError> var result = _orderService.ConfirmOrder(orderId);
if (result.TryGetResult(out var confirmation)) { Console.WriteLine($"Order confirmed: {confirmation.Id} at {confirmation.ConfirmedAt}"); } else if (result.TryGetError(out var error)) { Console.WriteLine($"Failed to confirm: [{error.Code}] {error.Description}"); } }
public string GetOrderStatus(OrderId orderId) { var result = _orderService.ConfirmOrder(orderId);
// Using Match() for functional composition return result.Match( confirmation => $"Order {confirmation.Id} confirmed at {confirmation.ConfirmedAt}", error => $"Error: [{error.Code}] {error.Description}"); }
public void ProcessOrderWithMatch(OrderId orderId) { var result = _orderService.ConfirmOrder(orderId);
// Switch() for side effects (Match returns a value; Switch is for actions) result.Switch( confirmation => { Console.WriteLine($"Success: Order {confirmation.Id} confirmed"); SendConfirmationEmail(confirmation); }, error => { Console.WriteLine($"Failed: {error.Description}"); LogError(error); }); }
public void HandleOrderWithPatternMatching(OrderId orderId) { var result = _orderService.ConfirmOrder(orderId);
// Pattern matching with switch expression var outcome = result switch { { IsSuccess: true } => $"Confirmed: {result.AsT0.Id}", { IsSuccess: false } when result.AsT1.Code == "NOT_FOUND" => "Order not found", { IsSuccess: false } => $"Error: {result.AsT1.Description}", _ => "Unknown state" };
Console.WriteLine(outcome); }}Key Points
Section titled “Key Points”- Use
Resultfor operations that can fail in predictable ways - Choose the appropriate variant based on your needs:
Result- Simple success/failure indicationResult<TError>- When you need to communicate why it failedResult<TResult, TError>- When success produces a value
- Use
Match()for functional composition and transforming results - Pattern matching with
switchexpressions enables conditional logic based on success/error states - Access underlying exception types with pattern guards for specific error handling
- Implicit conversions simplify creating results from values or errors
IsSuccessprovides a quick check before extracting valuesTryGetResultandTryGetErrorprovide safe access to the underlying values- Avoid exceptions for expected failure cases - use
Resultinstead