result-pattern
SKILL.md
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 methodscqrs-command-generator- Commands return Resultcqrs-query-generator- Queries return Resultpipeline-behaviors- Validation behavior uses Result
Weekly Installs
3
Repository
ronnythedev/dot…e-skillsGitHub Stars
42
First Seen
14 days ago
Security Audits
Installed on
opencode3
gemini-cli3
codebuddy3
github-copilot3
codex3
kimi-cli3