TurboMediator
Patterns

Result Pattern

Explicit error handling with Result types instead of exceptions

The TurboMediator.Result package provides Result<T> and Result<TValue, TError> types for explicit error handling without relying on exceptions for control flow.

Installation

dotnet add package TurboMediator.Result
using TurboMediator.Results;

Result<T>

A readonly struct that represents either a successful value or an error:

public readonly struct Result<TValue>
{
    public bool IsSuccess { get; }
    public bool IsFailure { get; }
    public TValue Value { get; }     // throws if failure
    public Exception Error { get; }  // throws if success
}

Creating Results

// Success
var success = Result.Success(42);
var success2 = Result.Success(new User("Alice"));

// Failure
var failure = Result.Failure<int>(new InvalidOperationException("oops"));

// From try-catch
var result = Result.Try(() => int.Parse("abc")); // Failure with FormatException
var asyncResult = await Result.TryAsync(() => httpClient.GetStringAsync(url));

Implicit Conversions

// Value → Success result
Result<int> result = 42;

// Exception → Failure result
Result<int> error = new InvalidOperationException("error");

Pattern Matching

var result = await mediator.Send(new GetUserQuery(userId));

// Match
var message = result.Match(
    onSuccess: user => $"Found {user.Name}",
    onFailure: error => $"Error: {error.Message}"
);

// Map — transform the success value
var nameResult = result.Map(user => user.Name);

// Bind — chain operations
var orderResult = result.Bind(user => GetOrdersForUser(user.Id));

Accessing Values

// Safe access with default
var user = result.GetValueOrDefault(User.Guest);

// Throw if failure
var user = result.GetValueOrThrow(); // throws the stored exception if failure

Result<TValue, TError>

A typed error variant for domain-specific errors without using exceptions:

public readonly struct Result<TValue, TError>
{
    public bool IsSuccess { get; }
    public bool IsFailure { get; }
    public TValue Value { get; }
    public TError Error { get; }
}

Practical Example

// Define domain errors
public abstract record UserError
{
    public record NotFound(Guid Id) : UserError;
    public record EmailTaken(string Email) : UserError;
    public record ValidationFailed(string[] Errors) : UserError;
}

// Use Result<T, TError> in handler
public record CreateUserCommand(string Name, string Email)
    : ICommand<Result<User, UserError>>;

public class CreateUserHandler
    : ICommandHandler<CreateUserCommand, Result<User, UserError>>
{
    private readonly IUserRepository _repo;

    public CreateUserHandler(IUserRepository repo) => _repo = repo;

    public async ValueTask<Result<User, UserError>> Handle(
        CreateUserCommand command, CancellationToken ct)
    {
        if (string.IsNullOrWhiteSpace(command.Name))
            return new UserError.ValidationFailed(new[] { "Name is required" });

        var existingUser = await _repo.FindByEmailAsync(command.Email, ct);
        if (existingUser is not null)
            return new UserError.EmailTaken(command.Email);

        var user = new User(Guid.NewGuid(), command.Name, command.Email);
        await _repo.AddAsync(user, ct);
        return user; // implicit conversion to success
    }
}

// Usage in API endpoint
app.MapPost("/users", async (CreateUserCommand cmd, IMediator mediator) =>
{
    var result = await mediator.Send(cmd);

    return result.Match(
        onSuccess: user => Results.Created($"/users/{user.Id}", user),
        onFailure: error => error switch
        {
            UserError.EmailTaken e => Results.Conflict($"Email {e.Email} is taken"),
            UserError.ValidationFailed v => Results.BadRequest(v.Errors),
            _ => Results.StatusCode(500)
        }
    );
});

The Result pattern is especially useful in domain-driven design where you want to represent expected failures (validation errors, not found, business rule violations) without throwing exceptions.

On this page