The Result pattern
An order that doesn’t exist is not an exceptional event. It’s an expected outcome. Mediant models it as
a value, not a throw.
using Mediant.Results;
// Successreturn Result<Guid>.Success(orderId);
// Failure — implicitly converted from an Errorreturn Error.NotFound("Order.NotFound", "Order not found.");Result and Result<T>
| Member | Meaning |
|---|---|
IsSuccess / IsFailure | Which branch this is |
Value | The value (throws if you read it on a failure — check first, or use Match) |
Error | The first error |
Errors | All errors (validation failures carry many) |
Result is the void-shaped variant, used by ICommand handlers that return nothing.
Typed errors
Error has a machine-readable Code, a human-readable Description, and a Type that determines how
the failure is treated downstream (including the HTTP status code, if you use Mediant.AspNetCore).
| Factory | ErrorType | HTTP status |
|---|---|---|
Error.Validation(code, description) | Validation | 400 (as a ValidationProblem) |
Error.Unauthorized(...) | Unauthorized | 401 |
Error.Forbidden(...) | Forbidden | 403 |
Error.NotFound(...) | NotFound | 404 |
Error.Conflict(...) | Conflict | 409 |
Error.Failure(...) | Failure | 422 |
Error.Unavailable(...) | Unavailable | 503 |
Error.Internal(...) | Internal | 500 |
Use a stable, namespaced code — Order.NotFound, not "not found". Clients switch on the code; the
description is for humans.
Composition
Result<T> is a monad in the useful sense: you can chain transformations, and the first failure
short-circuits the rest.
var greeting = await mediator.Send(new GetOrderById(id));
string message = greeting .Map(order => order.CustomerName) // Result<Order> -> Result<string> .Bind(name => ValidateName(name)) // Result<string> -> Result<string> .Match( name => $"Hello, {name}", // on success error => $"Error: {error.Description}" // on failure );Maptransforms the value.Func<T, TNew>.Bindchains an operation that itself may fail.Func<T, Result<TNew>>.Matchcollapses both branches into one value. It is the only way to read aResultthat cannot throw.
MapAsync and BindAsync take ValueTask-returning delegates; MatchAsync does the same for the two
branches.
Implicit conversions
Two conversions make handlers read cleanly:
public async ValueTask<Result<Order>> Handle(GetOrderById q, CancellationToken ct){ var order = await repo.FindAsync(q.Id, ct);
if (order is null) return Error.NotFound("Order.NotFound", $"Order {q.Id} was not found."); // Error -> Result<Order>
return order; // Order -> Result<Order>}Validation failures carry many errors
return Result.ValidationFailure<Guid>( new ValidationError("Email", "Email is required."), new ValidationError("Items", "An order must contain at least one item."));They surface in result.Errors, and Mediant.AspNetCore renders them as an RFC 7807
ValidationProblemDetails with one entry per field. The validation behavior does
this for you automatically when you use FluentValidation.
When to still throw
Throw for what is genuinely exceptional and unrecoverable at this layer: a corrupt configuration, a bug,
a violated invariant. The UnhandledException behavior catches, logs, and re-throws those — it never
swallows them into a Result.