A lightweight Result type for functional error handling in C#. Model success and failure explicitly, eliminate defensive exception handling, and compose operations safely with full async support.
Result<User, string> result = await GetUserAsync(id)
.BindAsync(user => ValidateEmailAsync(user))
.MapAsync(user => user with { IsVerified = true });
result.Match(
onSuccess: user => Console.WriteLine($"Welcome, {user.Name}"),
onFailure: error => Console.WriteLine($"Error: {error}")
);Requires .NET 10.0 or later.
dotnet add package OpaleThe core type is Result<T, TError> — a discriminated union that holds either a success value or an error, never both.
using Opale;
// Create results
Result<int, string> success = Result<int, string>.Ok(42);
Result<int, string> failure = Result<int, string>.Fail("Something went wrong");
// Implicit conversion from T
Result<int, string> implicit = 42; // equivalent to Ok(42)
// Check state
if (result.IsSuccess)
Console.WriteLine(result.Value);
if (result.IsFailure)
Console.WriteLine(result.Error);Use Result.Try to capture exceptions as typed failures:
using Opale.Static;
Result<int, Exception> result = Result.Try(() => int.Parse(userInput));| Method | Description |
|---|---|
Result<T, TError>.Ok(value) |
Creates a successful result |
Result<T, TError>.Fail(error) |
Creates a failed result |
implicit operator Result<T, TError>(T) |
Implicit conversion from T to Ok(value) |
Result.Try(Func<T>) |
Wraps a delegate, catching exceptions as Result<T, Exception> |
Execute different branches based on the result state.
// With return value
string message = result.Match(
onSuccess: value => $"Got {value}",
onFailure: error => $"Failed: {error}"
);
// Void branches
result.Match(
onSuccess: value => Console.WriteLine(value),
onFailure: () => Console.WriteLine("Failed")
);| Method | Description |
|---|---|
Match<TResult>(onSuccess, onFailure) |
Pattern match returning a value |
Match(onSuccess, onFailure) |
Pattern match for side effects |
Chain operations without unwrapping.
Result<string, string> result = Result<int, string>.Ok(42)
.Map(n => n * 2) // 84
.Map(n => n.ToString()) // "84"
.Bind(s => s.Length > 0
? Result<string, string>.Ok(s)
: Result<string, string>.Fail("Empty"));
// Transform the error type
Result<int, int> mapped = failedResult.MapError(err => err.Length);| Method | Description |
|---|---|
Map<TNew>(Func<T, TNew>) |
Transforms the success value |
Bind<TNew>(Func<T, Result<TNew, TError>>) |
Chains a result-returning operation |
MapError<TNewError>(Func<TError, TNewError>) |
Transforms the error value |
ToException(Func<TError, Exception>) |
Unwraps value or throws a mapped exception |
Provide fallback values or results on failure.
int value = result
.Recover(error => 0) // fallback value
.GetValueOrDefault(-1); // safe unwrap
Result<int, string> recovered = failedResult
.RecoverWith(error => Result<int, string>.Ok(0));| Method | Description |
|---|---|
Recover(Func<TError, T>) |
Returns an Ok result from the fallback value |
RecoverWith(Func<TError, Result<T, TError>>) |
Chains a fallback result |
GetValueOrDefault(T) |
Returns the value or a default on failure |
Execute actions without breaking the chain.
Result<User, string> result = await GetUserAsync(id)
.OnSuccess(user => logger.LogInfo($"Found user {user.Id}"))
.OnFailure(error => logger.LogError(error));| Method | Description |
|---|---|
OnSuccess(Action<T>) |
Executes an action if the result is successful |
OnFailure(Action<TError>) |
Executes an action if the result is a failure |
Tap(Action<T>) |
Alias for OnSuccess |
All side-effect methods return this, preserving the original result for further chaining.
Evaluate predicates against the result's value or error.
bool isAdult = result.IsSuccessAnd(user => user.Age >= 18);
bool isNotFound = result.IsFailureAnd(err => err == "NOT_FOUND");
bool hasValue = result.IsContains(expectedUser);| Method | Description |
|---|---|
IsSuccessAnd(Func<T, bool>) |
true if successful and predicate holds |
IsFailureAnd(Func<TError, bool>) |
true if failed and predicate holds |
IsContains(T) |
true if successful and value equals the given value |
// Returns the value or null/default on failure
User? user = result.ToOption();| Method | Description |
|---|---|
ToOption() |
Returns T? — the value on success, default on failure |
Bridge between Result and exception-based APIs.
// Throws if the result is a failure
User user = result.ToException(error => new NotFoundException(error));
// Wrap exception-prone code
Result<Data, Exception> safe = Result.Try(() => JsonSerializer.Deserialize<Data>(json));Every operation has a full async counterpart accepting and returning Task<Result<T, TError>>. This enables seamless railway-oriented pipelines in async code.
Result<UserDto, string> result = await GetUserAsync(id) // Task<Result<User, string>>
.BindAsync(user => FetchPermissionsAsync(user)) // async bind
.MapAsync(user => MapToDto(user)) // sync map in async context
.OnSuccessAsync(dto => auditLog.WriteAsync($"Read {dto.Id}")) // async side effect
.MatchAsync(
onSuccess: dto => dto,
onFailure: error => UserDto.Empty
);All sync methods have an *Async variant. Overloads accept both Func<T, Task<TResult>> and Func<T, TResult> so you can mix sync and async delegates freely.
MIT — see LICENSE for details.