Result<T, TError>
Result<T, TError> represents the outcome of an operation that can either succeed with a value of type T or fail with an error of type TError. The error type must inherit from the Error base record.
Creating Results
// Explicit construction
var success = Result.Success<int, ValidationError>(42);
var failure = Result.Failure<int, ValidationError>(
new ValidationError { Message = "Invalid input" });
// Implicit conversion
Result<int, ValidationError> result = 42; // Success
Result<int, ValidationError> result = new ValidationError { Message = "Oops" }; // Failure
Error Types
The library provides a hierarchy of error types:
public abstract record Error
{
public required string Message { get; init; }
public string? Code { get; init; }
public Dictionary<string, object>? Metadata { get; init; }
}
// Built-in subtypes
public sealed record ValidationError : Error { ... }
public sealed record NotFoundError : Error { ... }
public sealed record UnauthorizedError : Error { ... }
public sealed record ForbiddenError : Error { ... }
public sealed record ConflictError : Error { ... }
public sealed record ExternalServiceError : Error { ... }
public sealed record BadRequestError : Error { ... }
public sealed record InternalError : Error { ... }
Railway-Oriented Programming
Map
Transform the success value. Failures pass through unchanged:
var doubled = Result.Success<int, ValidationError>(21)
.Map(x => x * 2); // Success(42)
var failed = Result.Failure<int, ValidationError>(error)
.Map(x => x * 2); // Failure(error) — mapper never called
MapError
Transform the error value. Successes pass through unchanged:
var mapped = result.MapError(err =>
new InternalError { Message = $"Wrapped: {err.Message}" });
Bind
Chain operations that return Results. Short-circuits on first failure:
Result<User, ValidationError> ValidateUser(UserDto dto) => /* ... */;
Result<UserId, ValidationError> SaveUser(User user) => /* ... */;
var result = ValidateUser(dto)
.Bind(user => SaveUser(user)); // Success(userId) or first Failure
Match
Exhaustively handle both cases:
var message = result.Match(
success: value => $"Created user {value}",
failure: error => $"Failed: {error.Message}");
Extracting Values
// With a default
var value = result.GetValueOrDefault(0);
var value = result.GetValueOrDefault(() => ComputeDefault());
// Escape hatch — throws if failure
var value = result.GetValueOrThrow();
// Alternative result on failure
var value = result.OrElse(fallbackResult);
var value = result.OrElse(() => ComputeFallback());
Side Effects
result
.Tap(value => logger.LogInformation("Success: {Value}", value))
.TapError(error => logger.LogError("Failed: {Error}", error.Message));
LINQ Support
var result =
from user in ValidateUser(dto)
from saved in SaveUser(user)
select saved;
Async Operations
var result = await ValidateUserAsync(dto)
.BindAsync(user => SaveUserAsync(user))
.MapAsync(id => EnrichAsync(id))
.Match(
success: x => $"Done: {x}",
failure: e => $"Error: {e.Message}");
Task Extensions
Chain operations on Task<Result<T, TError>> without intermediate awaits:
using DarkPeak.Functional.Extensions;
var result = await FetchUserAsync(id) // Task<Result<User, Error>>
.Map(user => user.Email) // Task<Result<string, Error>>
.Bind(email => ValidateAsync(email)) // Task<Result<Email, Error>>
.Tap(email => Log(email)) // side effect
.Match(
success: e => $"Valid: {e}",
failure: e => $"Error: {e.Message}");