Result Pattern Implementation
Overview
The Result pattern provides explicit error handling without exceptions:
-
No exceptions for business errors - Exceptions for truly exceptional cases only
-
Explicit success/failure - Compiler forces handling of both cases
-
Composable errors - Chain operations, fail fast
-
Self-documenting - Method signatures show possible outcomes
Quick Reference
Type Purpose Usage
Result
Operation without return value Update, Delete operations
Result<T>
Operation with return value Create, Get operations
Error
Error information Code + Description
Implementation Structure
/Domain/Abstractions/ ├── Result.cs # Result and Result<T> ├── Error.cs # Error record └── ValidationResult.cs # Multiple errors support
Template: Core Result Types
// src/{name}.domain/Abstractions/Error.cs namespace {name}.domain.abstractions;
/// <summary> /// Represents an error with a code and description /// </summary> public record Error(string Code, string Description) { /// <summary> /// Represents no error (success state) /// </summary> public static readonly Error None = new(string.Empty, string.Empty);
/// <summary>
/// Represents a null value error
/// </summary>
public static readonly Error NullValue = new(
"Error.NullValue",
"A null value was provided");
/// <summary>
/// Creates an error from an exception
/// </summary>
public static Error FromException(Exception exception) => new(
"Error.Exception",
exception.Message);
/// <summary>
/// Implicit conversion to string (returns Code)
/// </summary>
public static implicit operator string(Error error) => error.Code;
public override string ToString() => Code;
}
// src/{name}.domain/Abstractions/Result.cs namespace {name}.domain.abstractions;
/// <summary> /// Represents the outcome of an operation that doesn't return a value /// </summary> public class Result { protected Result(bool isSuccess, Error error) { if (isSuccess && error != Error.None) { throw new InvalidOperationException( "Cannot create successful result with an error"); }
if (!isSuccess && error == Error.None)
{
throw new InvalidOperationException(
"Cannot create failed result without an error");
}
IsSuccess = isSuccess;
Error = error;
}
public bool IsSuccess { get; }
public bool IsFailure => !IsSuccess;
public Error Error { get; }
// ═══════════════════════════════════════════════════════════════
// FACTORY METHODS
// ═══════════════════════════════════════════════════════════════
/// <summary>
/// Creates a successful result
/// </summary>
public static Result Success() => new(true, Error.None);
/// <summary>
/// Creates a failed result with the specified error
/// </summary>
public static Result Failure(Error error) => new(false, error);
/// <summary>
/// Creates a successful result with a value
/// </summary>
public static Result<TValue> Success<TValue>(TValue value) =>
new(value, true, Error.None);
/// <summary>
/// Creates a failed result with the specified error
/// </summary>
public static Result<TValue> Failure<TValue>(Error error) =>
new(default, false, error);
/// <summary>
/// Creates a result based on a condition
/// </summary>
public static Result Create(bool condition, Error error) =>
condition ? Success() : Failure(error);
/// <summary>
/// Creates a result based on a condition with a value
/// </summary>
public static Result<TValue> Create<TValue>(TValue? value, Error error) =>
value is not null ? Success(value) : Failure<TValue>(error);
}
/// <summary> /// Represents the outcome of an operation that returns a value /// </summary> public class Result<TValue> : Result { private readonly TValue? _value;
protected internal Result(TValue? value, bool isSuccess, Error error)
: base(isSuccess, error)
{
_value = value;
}
/// <summary>
/// Gets the value if successful, throws if failed
/// </summary>
public TValue Value => IsSuccess
? _value!
: throw new InvalidOperationException(
$"Cannot access value of a failed result. Error: {Error.Code}");
/// <summary>
/// Implicit conversion from value to successful Result
/// </summary>
public static implicit operator Result<TValue>(TValue? value) =>
value is not null ? Success(value) : Failure<TValue>(Error.NullValue);
/// <summary>
/// Implicit conversion from Error to failed Result
/// </summary>
public static implicit operator Result<TValue>(Error error) =>
Failure<TValue>(error);
}
Template: Result Extensions (Functional Operations)
// src/{name}.domain/Abstractions/ResultExtensions.cs namespace {name}.domain.abstractions;
public static class ResultExtensions { // ═══════════════════════════════════════════════════════════════ // MAP: Transform success value // ═══════════════════════════════════════════════════════════════
/// <summary>
/// Transforms the value if successful, preserves error if failed
/// </summary>
public static Result<TOut> Map<TIn, TOut>(
this Result<TIn> result,
Func<TIn, TOut> mapper)
{
return result.IsSuccess
? Result.Success(mapper(result.Value))
: Result.Failure<TOut>(result.Error);
}
/// <summary>
/// Async version of Map
/// </summary>
public static async Task<Result<TOut>> Map<TIn, TOut>(
this Task<Result<TIn>> resultTask,
Func<TIn, TOut> mapper)
{
var result = await resultTask;
return result.Map(mapper);
}
// ═══════════════════════════════════════════════════════════════
// BIND: Chain operations that return Result
// ═══════════════════════════════════════════════════════════════
/// <summary>
/// Chains another Result-returning operation if successful
/// </summary>
public static Result<TOut> Bind<TIn, TOut>(
this Result<TIn> result,
Func<TIn, Result<TOut>> binder)
{
return result.IsSuccess
? binder(result.Value)
: Result.Failure<TOut>(result.Error);
}
/// <summary>
/// Async version of Bind
/// </summary>
public static async Task<Result<TOut>> Bind<TIn, TOut>(
this Result<TIn> result,
Func<TIn, Task<Result<TOut>>> binder)
{
return result.IsSuccess
? await binder(result.Value)
: Result.Failure<TOut>(result.Error);
}
/// <summary>
/// Async version of Bind for Task results
/// </summary>
public static async Task<Result<TOut>> Bind<TIn, TOut>(
this Task<Result<TIn>> resultTask,
Func<TIn, Result<TOut>> binder)
{
var result = await resultTask;
return result.Bind(binder);
}
/// <summary>
/// Fully async Bind
/// </summary>
public static async Task<Result<TOut>> Bind<TIn, TOut>(
this Task<Result<TIn>> resultTask,
Func<TIn, Task<Result<TOut>>> binder)
{
var result = await resultTask;
return await result.Bind(binder);
}
// ═══════════════════════════════════════════════════════════════
// TAP: Execute side effect without changing result
// ═══════════════════════════════════════════════════════════════
/// <summary>
/// Executes an action if successful, returns original result
/// </summary>
public static Result<T> Tap<T>(
this Result<T> result,
Action<T> action)
{
if (result.IsSuccess)
{
action(result.Value);
}
return result;
}
/// <summary>
/// Async version of Tap
/// </summary>
public static async Task<Result<T>> Tap<T>(
this Result<T> result,
Func<T, Task> action)
{
if (result.IsSuccess)
{
await action(result.Value);
}
return result;
}
// ═══════════════════════════════════════════════════════════════
// MATCH: Pattern match on result
// ═══════════════════════════════════════════════════════════════
/// <summary>
/// Executes success or failure function based on result state
/// </summary>
public static TOut Match<TIn, TOut>(
this Result<TIn> result,
Func<TIn, TOut> onSuccess,
Func<Error, TOut> onFailure)
{
return result.IsSuccess
? onSuccess(result.Value)
: onFailure(result.Error);
}
/// <summary>
/// Async version of Match
/// </summary>
public static async Task<TOut> Match<TIn, TOut>(
this Task<Result<TIn>> resultTask,
Func<TIn, TOut> onSuccess,
Func<Error, TOut> onFailure)
{
var result = await resultTask;
return result.Match(onSuccess, onFailure);
}
// ═══════════════════════════════════════════════════════════════
// ENSURE: Add validation to existing result
// ═══════════════════════════════════════════════════════════════
/// <summary>
/// Validates the value and fails if predicate returns false
/// </summary>
public static Result<T> Ensure<T>(
this Result<T> result,
Func<T, bool> predicate,
Error error)
{
if (result.IsFailure)
{
return result;
}
return predicate(result.Value)
? result
: Result.Failure<T>(error);
}
/// <summary>
/// Async version of Ensure
/// </summary>
public static async Task<Result<T>> Ensure<T>(
this Result<T> result,
Func<T, Task<bool>> predicate,
Error error)
{
if (result.IsFailure)
{
return result;
}
return await predicate(result.Value)
? result
: Result.Failure<T>(error);
}
// ═══════════════════════════════════════════════════════════════
// COMBINE: Combine multiple results
// ═══════════════════════════════════════════════════════════════
/// <summary>
/// Combines multiple results, returning first failure or success
/// </summary>
public static Result Combine(params Result[] results)
{
foreach (var result in results)
{
if (result.IsFailure)
{
return result;
}
}
return Result.Success();
}
/// <summary>
/// Combines multiple results with values
/// </summary>
public static Result<(T1, T2)> Combine<T1, T2>(
Result<T1> result1,
Result<T2> result2)
{
if (result1.IsFailure) return Result.Failure<(T1, T2)>(result1.Error);
if (result2.IsFailure) return Result.Failure<(T1, T2)>(result2.Error);
return Result.Success((result1.Value, result2.Value));
}
/// <summary>
/// Combines three results with values
/// </summary>
public static Result<(T1, T2, T3)> Combine<T1, T2, T3>(
Result<T1> result1,
Result<T2> result2,
Result<T3> result3)
{
if (result1.IsFailure) return Result.Failure<(T1, T2, T3)>(result1.Error);
if (result2.IsFailure) return Result.Failure<(T1, T2, T3)>(result2.Error);
if (result3.IsFailure) return Result.Failure<(T1, T2, T3)>(result3.Error);
return Result.Success((result1.Value, result2.Value, result3.Value));
}
// ═══════════════════════════════════════════════════════════════
// GET VALUE OR DEFAULT
// ═══════════════════════════════════════════════════════════════
/// <summary>
/// Returns the value if successful, or default value if failed
/// </summary>
public static T GetValueOrDefault<T>(
this Result<T> result,
T defaultValue = default!)
{
return result.IsSuccess ? result.Value : defaultValue;
}
/// <summary>
/// Returns the value if successful, or result of factory if failed
/// </summary>
public static T GetValueOrDefault<T>(
this Result<T> result,
Func<T> defaultFactory)
{
return result.IsSuccess ? result.Value : defaultFactory();
}
}
Template: Validation Result (Multiple Errors)
// src/{name}.domain/Abstractions/ValidationResult.cs namespace {name}.domain.abstractions;
/// <summary> /// Result that can contain multiple validation errors /// </summary> public sealed class ValidationResult : Result, IValidationResult { private ValidationResult(Error[] errors) : base(false, IValidationResult.ValidationError) { Errors = errors; }
public Error[] Errors { get; }
public static ValidationResult WithErrors(Error[] errors) => new(errors);
}
/// <summary> /// Validation result with a value /// </summary> public sealed class ValidationResult<TValue> : Result<TValue>, IValidationResult { private ValidationResult(Error[] errors) : base(default, false, IValidationResult.ValidationError) { Errors = errors; }
public Error[] Errors { get; }
public static ValidationResult<TValue> WithErrors(Error[] errors) => new(errors);
}
/// <summary> /// Marker interface for validation results /// </summary> public interface IValidationResult { public static readonly Error ValidationError = new( "Validation.Error", "One or more validation errors occurred");
Error[] Errors { get; }
}
Usage Examples
Basic Usage in Domain Entity
public sealed class User : Entity { public static Result<User> Create(string email, string name) { // Validate email var emailResult = Email.Create(email); if (emailResult.IsFailure) { return Result.Failure<User>(emailResult.Error); }
// Validate name
if (string.IsNullOrWhiteSpace(name))
{
return Result.Failure<User>(UserErrors.NameRequired);
}
if (name.Length > 100)
{
return Result.Failure<User>(UserErrors.NameTooLong);
}
var user = new User(Guid.NewGuid(), emailResult.Value, name);
return Result.Success(user);
}
public Result UpdateName(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
return Result.Failure(UserErrors.NameRequired);
}
Name = name;
return Result.Success();
}
}
Usage in Command Handler
internal sealed class CreateUserCommandHandler : ICommandHandler<CreateUserCommand, Guid> { private readonly IUserRepository _userRepository; private readonly IUnitOfWork _unitOfWork;
public async Task<Result<Guid>> Handle(
CreateUserCommand request,
CancellationToken cancellationToken)
{
// Check if user exists
var existingUser = await _userRepository
.GetByEmailAsync(request.Email, cancellationToken);
if (existingUser is not null)
{
return Result.Failure<Guid>(UserErrors.EmailAlreadyExists);
}
// Create user using factory method
var userResult = User.Create(request.Email, request.Name);
if (userResult.IsFailure)
{
return Result.Failure<Guid>(userResult.Error);
}
_userRepository.Add(userResult.Value);
await _unitOfWork.SaveChangesAsync(cancellationToken);
return userResult.Value.Id;
}
}
Chaining with Bind
public async Task<Result<OrderConfirmation>> PlaceOrder( Guid userId, CreateOrderRequest request, CancellationToken ct) { return await GetUser(userId, ct) .Bind(user => ValidateUserCanOrder(user)) .Bind(user => CreateOrder(user, request)) .Bind(order => ProcessPayment(order, ct)) .Bind(order => SendConfirmation(order, ct)); }
Using Match in Controller
[HttpPost] public async Task<IActionResult> Create( [FromBody] CreateUserRequest request, CancellationToken ct) { var command = new CreateUserCommand(request.Email, request.Name);
var result = await _sender.Send(command, ct);
return result.Match(
onSuccess: id => CreatedAtAction(nameof(GetById), new { id }, id),
onFailure: error => error.Code switch
{
"User.EmailExists" => Conflict(error),
"User.NotFound" => NotFound(error),
_ => BadRequest(error)
});
}
Combining Multiple Results
public Result<Order> CreateOrder( CreateOrderRequest request) { // Validate all fields var customerResult = CustomerId.Create(request.CustomerId); var addressResult = Address.Create(request.Street, request.City); var amountResult = Money.Create(request.Amount);
// Combine - returns first failure
var combinedResult = ResultExtensions.Combine(
customerResult,
addressResult,
amountResult);
if (combinedResult.IsFailure)
{
return Result.Failure<Order>(combinedResult.Error);
}
var (customerId, address, amount) = combinedResult.Value;
return Order.Create(customerId, address, amount);
}
Domain Errors Pattern
// src/{name}.domain/Users/UserErrors.cs namespace {name}.domain.users;
public static class UserErrors { public static readonly Error NotFound = new( "User.NotFound", "The user with the specified ID was not found");
public static readonly Error EmailAlreadyExists = new(
"User.EmailExists",
"A user with this email already exists");
public static readonly Error NameRequired = new(
"User.NameRequired",
"User name is required");
public static readonly Error NameTooLong = new(
"User.NameTooLong",
"User name cannot exceed 100 characters");
public static readonly Error InvalidCredentials = new(
"User.InvalidCredentials",
"The provided credentials are invalid");
public static readonly Error AccountLocked = new(
"User.AccountLocked",
"The user account is locked");
// Parameterized errors
public static Error NotFoundById(Guid id) => new(
"User.NotFound",
$"The user with ID '{id}' was not found");
public static Error Unauthorized(string resource) => new(
"User.Unauthorized",
$"User is not authorized to access '{resource}'");
}
Critical Rules
-
Never throw for business errors - Return Result.Failure
-
Always check IsSuccess/IsFailure - Before accessing Value
-
Use factory methods - Result.Success() , Result.Failure()
-
Errors are immutable - record Error(...)
-
Error codes are unique - Follow {Entity}.{ErrorType} pattern
-
Chain with Bind - For sequential operations
-
Use Match in controllers - Clean response mapping
-
Value objects validate in Create - Return Result from factories
-
Combine for multiple validations - Returns first failure
-
Keep Result in domain - Don't leak to API layer directly
Anti-Patterns to Avoid
// ❌ WRONG: Throwing for business errors if (user is null) throw new NotFoundException("User not found");
// ✅ CORRECT: Return Result if (user is null) return Result.Failure<User>(UserErrors.NotFound);
// ❌ WRONG: Accessing Value without checking var user = result.Value; // Throws if failed!
// ✅ CORRECT: Check first if (result.IsFailure) return Result.Failure(result.Error); var user = result.Value;
// ❌ WRONG: Ignoring Result await CreateUserAsync(request); // Ignores possible failure
// ✅ CORRECT: Handle the result var result = await CreateUserAsync(request); if (result.IsFailure) // Handle error
// ❌ WRONG: Using exceptions as control flow try { return Success(Process()); } catch (ValidationException ex) { return Failure(ex.Error); }
// ✅ CORRECT: Design to return Result var validationResult = Validate(input); if (validationResult.IsFailure) return validationResult; return Success(Process(input));
Related Skills
-
domain-entity-generator
-
Use Result in factory methods
-
cqrs-command-generator
-
Commands return Result
-
cqrs-query-generator
-
Queries return Result
-
pipeline-behaviors
-
Validation behavior uses Result