🎯 Typed Error Pipelines
Use Result<TValue, TError> when you want compile-time-known, exhaustive failure edges — every error type in the pipeline is explicit in the signature. Use Result<T> when you prefer a shared IEnumerable<IError> bag and runtime polymorphism (e.g. FluentValidation-style lists).
ErrorsOf<T1..T8> — Typed Error Union
ErrorsOf<T1, T2, ...> is a discriminated union over error types. It inherits OneOfBase (same shared dispatch as OneOf) and implements IError — so it is itself a valid error usable as TError in Result<TValue, TError>.
All type slots must implement IError (where T1 : IError where T2 : IError ...).
// Implicit conversions from each Ti — no explicit wrapping needed
ErrorsOf<ValidationError, InventoryError> err = new ValidationError("Amount required");
// Exhaustive match over the active case
string message = err.Match(
v => v.Message,
i => i.Message);
// IError implementation delegates to the active case
Console.WriteLine(err.Message); // "Amount required"
// IsT1..T8 / AsT1..T8
if (err.IsT2) { var inv = err.AsT2; /* InventoryError */ }
Result<TValue, TError> — Factory and Accessors
// Factory
var ok = Result<Order, ValidationError>.Ok(order);
var fail = Result<Order, ValidationError>.Fail(new ValidationError("Amount required"));
// Accessors
bool succeeded = ok.IsSuccess; // true
bool failed = ok.IsFailure; // false
Order value = ok.Value; // throws InvalidOperationException on failure
ValidationError err = fail.Error; // throws InvalidOperationException on success
Full Pipeline Walkthrough
Each step declares a single concrete error type. Bind widens the union by exactly one slot per step — the error surface grows automatically:
// Steps — clean single-error signatures
Result<Order, ValidationError> Validate(CheckoutRequest req) => ...
Result<Order, InventoryError> ReserveInventory(Order order) => ...
Result<Order, PaymentError> ProcessPayment(Order order) => ...
Result<Order, DatabaseError> CreateOrder(Order order) => ...
// Pipeline — union grows: E1 → E1,E2 → E1,E2,E3 → E1,E2,E3,E4
Result<Order, ErrorsOf<ValidationError, InventoryError, PaymentError, DatabaseError>>
Checkout(CheckoutRequest request) =>
Validate(request)
.Bind(ReserveInventory)
.Bind(ProcessPayment)
.Bind(CreateOrder);
// Callsite — exhaustive match, compile-time safe
var result = Checkout(request);
result.Error.Match(
v => HandleValidation(v),
i => HandleInventory(i),
p => HandlePayment(p),
d => HandleDatabase(d));
Operations Table
| Method | Effect |
|---|---|
Bind(next) ×7 |
Chain step, grow union by one slot |
Map(mapper) |
Transform value, error type unchanged |
Tap(action) |
Side effect on success, returns original result |
TapOnFailure(action) |
Side effect on failure, returns original result |
Ensure(pred, error) ×7 |
Guard — widen union by one slot if predicate fails |
EnsureAsync(pred, error) ×7 |
Async guard — same widening |
MapError(mapper) |
Translate error surface (TErrorIn → TErrorOut) |
Ensure — Guard with Union Widening
Ensure adds a guard condition. Each call widens the error union by one slot when the predicate fails:
Result<Order, ValidationError> Validate(CheckoutRequest req) => ...
// First Ensure: Result<Order, ValidationError>
// → Result<Order, ErrorsOf<ValidationError, CreditLimitError>>
var guarded = Validate(request)
.Ensure(order => order.Amount > 0, new CreditLimitError("Amount must be positive"))
.Ensure(order => order.Amount < 10_000, new CreditLimitError("Amount exceeds limit"));
// EnsureAsync — predicate is async
var asyncGuarded = await result
.EnsureAsync(order => CheckCreditAsync(order), new CreditLimitError("Credit check failed"));
MapError — Layer-Boundary Translation
Collapse or translate the error union at layer boundaries — e.g. convert a domain union into a single API error type:
// Collapse union into a single domain error for the HTTP layer
Result<Order, DomainError> adapted = result.MapError(e => e.Match(
v => new DomainError(v.Message),
i => new DomainError(i.Message),
p => new DomainError(p.Message),
d => new DomainError(d.Message)));
Result.Flow — Type-Read Mode
When a [ResultFlow]-annotated method returns Result<T, TError>, failure edges in the generated Mermaid diagram show the exact error type — no body scanning needed. The generator reads TypeArguments[1] directly from the Roslyn return type symbol.
N0_Bind["Bind<br/>Order → Order"]:::transform
N0_Bind -->|"fail: ErrorsOf<ValidationError, InventoryError>"| F0["Failure"]:::failure
See the ResultFlow section for the full [ResultFlow] / Mermaid diagram feature.