Skip to content

Result Pattern

Overview

The Result Pattern is a functional programming concept that provides a clean and type-safe way to handle success and failure cases in your applications. TinyResult implements this pattern in a way that is both powerful and easy to use.

Key Concepts

1. Result Type

The Result<T> type represents the outcome of an operation that can either succeed or fail:

public class Result<T>
{
    public bool IsSuccess { get; }
    public bool IsFailure => !IsSuccess;
    public T Value { get; }
    public Error Error { get; }
}

2. Error Type

The Error type represents detailed information about a failure:

public class Error
{
    public ErrorCode Code { get; }
    public string Message { get; }
    public IReadOnlyDictionary<string, object> Metadata { get; }
}

3. Error Codes

Predefined error codes for common failure scenarios:

public enum ErrorCode
{
    Unknown,
    NotFound,
    ValidationError,
    Unauthorized,
    InvalidOperation,
    NetworkError,
    Timeout,
    ConfigurationError,
    DatabaseError
}

Core Operations

1. Creating Results

// Success
var success = Result<int>.Success(42);

// Failure with message
var failure = Result<int>.Failure("Something went wrong");

// Failure with error code
var failure = Result<int>.Failure(ErrorCode.NotFound, "User not found");

// Failure with custom error
var failure = Result<int>.Failure(new Error(ErrorCode.ValidationError, "Invalid input"));

2. Transforming Results

// Map: Transform success value
var result = success.Map(x => x * 2);

// Bind: Chain operations that return results
var result = success.Bind(x => GetUser(x));

// Filter: Validate success value
var result = success.Filter(x => x > 0, "Value must be positive");

3. Handling Results

// Match: Handle both success and failure
result.Match(
    value => Console.WriteLine($"Success: {value}"),
    error => Console.WriteLine($"Error: {error.Message}")
);

// OnSuccess: Handle success only
result.OnSuccess(value => Console.WriteLine($"Success: {value}"));

// OnFailure: Handle failure only
result.OnFailure(error => Console.WriteLine($"Error: {error.Message}"));

Advanced Features

1. Result Pipelines

Chain multiple operations together:

var result = GetUser(1)
    .Map(user => user.Name)
    .Map(name => name.ToUpper())
    .Filter(name => name.Length > 0, "Name cannot be empty")
    .OnSuccess(name => Console.WriteLine($"Name: {name}"))
    .OnFailure(error => Console.WriteLine($"Error: {error.Message}"));

2. Error Handling

Handle errors in a type-safe way:

var result = GetUser(1);

if (result.IsFailure)
{
    switch (result.Error.Code)
    {
        case ErrorCode.NotFound:
            return Result<User>.Failure("User not found. Please try again.");
        case ErrorCode.ValidationError:
            return Result<User>.Failure("Invalid user data.");
        default:
            return Result<User>.Failure("An unexpected error occurred.");
    }
}

3. Result Aggregation

Combine multiple results:

var results = new[]
{
    GetUser(1),
    GetUser(2),
    GetUser(3)
};

var combinedResult = Result.Combine(results);

combinedResult.Match(
    users => Console.WriteLine($"Found {users.Count()} users"),
    error => Console.WriteLine($"Error: {error.Message}")
);

Best Practices

1. Use Results Instead of Exceptions

// Avoid
public User GetUser(int id)
{
    var user = _repository.GetById(id);
    if (user == null)
    {
        throw new UserNotFoundException($"User {id} not found");
    }
    return user;
}

// Prefer
public Result<User> GetUser(int id)
{
    var user = _repository.GetById(id);
    return user != null
        ? Result<User>.Success(user)
        : Result<User>.Failure(ErrorCode.NotFound, $"User {id} not found");
}

2. Chain Operations

// Avoid
var user = GetUser(1);
if (user.IsSuccess)
{
    var address = GetAddress(user.Value.Id);
    if (address.IsSuccess)
    {
        return address.Value;
    }
    return Result<Address>.Failure(address.Error);
}
return Result<Address>.Failure(user.Error);

// Prefer
return GetUser(1)
    .Bind(user => GetAddress(user.Id));

3. Use Descriptive Error Messages

// Avoid
return Result<User>.Failure("Error");

// Prefer
return Result<User>.Failure(
    ErrorCode.ValidationError,
    "User name must be between 3 and 50 characters",
    new Dictionary<string, object>
    {
        { "Field", "Name" },
        { "MinLength", 3 },
        { "MaxLength", 50 }
    }
);

Common Use Cases

1. API Responses

public async Task<IActionResult> GetUser(int id)
{
    var result = await _userService.GetUser(id);
    return result.Match(
        user => Ok(user),
        error => StatusCode(GetStatusCode(error.Code), error.Message)
    );
}

2. Validation

public Result<User> ValidateUser(User user)
{
    var validationResult = ValidationResult.Create();

    if (string.IsNullOrEmpty(user.Name))
    {
        validationResult.AddError("Name", "Name is required");
    }

    if (user.Age < 18)
    {
        validationResult.AddError("Age", "User must be at least 18 years old");
    }

    return validationResult.IsValid
        ? Result<User>.Success(user)
        : Result<User>.Failure(validationResult);
}

3. Database Operations

public async Task<Result<User>> CreateUser(User user)
{
    try
    {
        var createdUser = await _repository.CreateAsync(user);
        return Result<User>.Success(createdUser);
    }
    catch (Exception ex)
    {
        return Result<User>.Failure(
            ErrorCode.DatabaseError,
            "Failed to create user",
            new Dictionary<string, object> { { "Exception", ex } }
        );
    }
}

Next Steps