Skip to content

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.

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.

Return Result to indicate operation outcomes:

// Simple Result
public 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 error
public 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 types
public 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);
}
}

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);
}
}
  • Use Result for operations that can fail in predictable ways
  • Choose the appropriate variant based on your needs:
    • Result - Simple success/failure indication
    • Result<TError> - When you need to communicate why it failed
    • Result<TResult, TError> - When success produces a value
  • Use Match() for functional composition and transforming results
  • Pattern matching with switch expressions 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
  • IsSuccess provides a quick check before extracting values
  • TryGetResult and TryGetError provide safe access to the underlying values
  • Avoid exceptions for expected failure cases - use Result instead