Skip to content

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;
// Success
return Result<Guid>.Success(orderId);
// Failure — implicitly converted from an Error
return Error.NotFound("Order.NotFound", "Order not found.");

Result and Result<T>

MemberMeaning
IsSuccess / IsFailureWhich branch this is
ValueThe value (throws if you read it on a failure — check first, or use Match)
ErrorThe first error
ErrorsAll 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).

FactoryErrorTypeHTTP status
Error.Validation(code, description)Validation400 (as a ValidationProblem)
Error.Unauthorized(...)Unauthorized401
Error.Forbidden(...)Forbidden403
Error.NotFound(...)NotFound404
Error.Conflict(...)Conflict409
Error.Failure(...)Failure422
Error.Unavailable(...)Unavailable503
Error.Internal(...)Internal500

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
);
  • Map transforms the value. Func<T, TNew>.
  • Bind chains an operation that itself may fail. Func<T, Result<TNew>>.
  • Match collapses both branches into one value. It is the only way to read a Result that 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.