A lightweight, thread-safe Result pattern implementation for .NET that provides a clean, functional approach to error handling without exceptions.
- β Simple and Intuitive API - Easy to understand and use
- β Thread-Safe - Immutable design ensures thread safety
- β No Dependencies - Lightweight with zero external dependencies
- β Async Support - First-class support for async/await patterns
- β
Functional Composition - Chain operations with
ThenandWhen - β
Type-Safe - Generic
Result<T>for operations that return values - β Error Aggregation - Combine multiple results and aggregate errors
- β Implicit Conversions - Seamless conversion between values and Results
dotnet add package Tethys.Resultsusing Tethys.Results;
// Simple success/failure results
Result successResult = Result.Ok("Success message");
Result failureResult = Result.Fail("Something went wrong");
// Results with values
Result<string> valueResult = Result<string>.Ok("Hello World", "Operation completed");
Result<int> errorResult = Result<int>.Fail("Not found");
// Results with exceptions
var exception = new Exception("Test exception");
Result failWithException = Result.Fail("Error message", exception);// Chain multiple operations that depend on each other
var result = Result.Ok("Start")
.Then(() =>
{
// Do some work
return Result.Ok("First operation completed");
})
.Then(() =>
{
// Do more work
return Result.Ok("Second operation completed");
})
.Then(() =>
{
// Final operation
return Result.Ok("All operations completed");
});
if (result.Success)
{
Console.WriteLine($"Success: {result.Message}");
}
else
{
Console.WriteLine($"Error: {result.Message}");
}// Transform data through a pipeline
var result = Result<int>.Ok(42, "Initial value")
.Then(value =>
{
// Transform the value
return Result<string>.Ok(value.ToString(), "Converted to string");
})
.Then(str =>
{
// Further transformation
return Result<string>.Ok($"The answer is: {str}", "Formatted result");
});
// Extract the final value
string finalValue = result.GetValueOrDefault("No answer available");// Chain async operations seamlessly
var result = await Result.Ok("Start")
.ThenAsync(async () =>
{
await Task.Delay(100); // Simulate async work
return Result.Ok("Async operation completed");
})
.ThenAsync(async () =>
{
await Task.Delay(100); // More async work
return Result<string>.Ok("Final result", "All async operations done");
});
// Work with Task<Result> directly
Task<Result<int>> asyncResult = Task.FromResult(Result<int>.Ok(42, "From async"));
var processed = await asyncResult.ThenAsync(async value =>
{
await Task.Delay(100);
return Result<string>.Ok($"Processed: {value}", "Transformation complete");
});// Execute operations conditionally
var result = Result.Ok("Initial state")
.When(true, () => Result.Ok("Condition was true"))
.When(false, () => Result.Ok("This won't execute"));
// Conditional execution with data
var dataResult = Result<int>.Ok(10)
.When(true, () => Result<int>.Ok(20))
.Then(value => Result<int>.Ok(value * 2)); // Results in 40// Combine multiple validation results
var results = new List<Result>
{
ValidateEmail("user@example.com"),
ValidatePassword("SecurePass123!"),
ValidateUsername("johndoe")
};
var combined = Result.Combine(results);
if (!combined.Success)
{
// Access aggregated errors
var aggregateError = combined.Exception as AggregateError;
foreach (var error in aggregateError.ErrorMessages)
{
Console.WriteLine($"Validation error: {error}");
}
}
// Combine results with data
var dataResults = new List<Result<int>>
{
Result<int>.Ok(1),
Result<int>.Ok(2),
Result<int>.Ok(3)
};
var combinedData = Result<int>.Combine(dataResults);
if (combinedData.Success)
{
var sum = combinedData.Data.Sum(); // Sum is 6
}Result<User> userResult = GetUser(userId);
// Get value or default
User user = userResult.GetValueOrDefault(new User { Name = "Guest" });
// Try pattern
if (userResult.TryGetValue(out User foundUser))
{
Console.WriteLine($"Found user: {foundUser.Name}");
}
else
{
Console.WriteLine("User not found");
}
// Get value or throw (use sparingly)
try
{
User user = userResult.GetValueOrThrow();
// Use the user
}
catch (InvalidOperationException ex)
{
// Handle the error
Console.WriteLine($"Failed to get user: {ex.Message}");
}// Implicitly convert values to Results
Result<int> implicitResult = 42; // Creates Result<int>.Ok(42)
Result<string> stringResult = "Hello"; // Creates Result<string>.Ok("Hello")
// Implicitly convert successful Results to values (throws if failed)
Result<int> successResult = Result<int>.Ok(42);
int value = successResult; // Gets 42
// Use in expressions
Result<int> result1 = Result<int>.Ok(10);
Result<int> result2 = Result<int>.Ok(20);
int sum = result1 + result2; // Implicit conversion, sum is 30Result.Ok()- Creates a successful resultResult.Ok(string message)- Creates a successful result with a messageResult.Fail(string message)- Creates a failed result with an error messageResult.Fail(string message, Exception exception)- Creates a failed result with message and exceptionResult.Fail(Exception exception)- Creates a failed result from an exceptionResult.Combine(IEnumerable<Result> results)- Combines multiple results into one
Result<T>.Ok(T value)- Creates a successful result with a valueResult<T>.Ok(T value, string message)- Creates a successful result with a value and messageResult<T>.Fail(string message)- Creates a failed result with an error messageResult<T>.Fail(string message, Exception exception)- Creates a failed result with message and exceptionResult<T>.Fail(Exception exception)- Creates a failed result from an exceptionResult<T>.Combine(IEnumerable<Result<T>> results)- Combines multiple results with values
Then(Func<Result> operation)- Chains operations on successful resultsThen<T>(Func<Result<T>> operation)- Chains operations that return valuesThenAsync(Func<Task<Result>> operation)- Chains async operationsThenAsync<T>(Func<Task<Result<T>>> operation)- Chains async operations that return valuesWhen(bool condition, Func<Result> operation)- Conditionally executes operationsGetValueOrDefault(T defaultValue = default)- Gets the value or a defaultTryGetValue(out T value)- Tries to get the value using the Try patternGetValueOrThrow()- Gets the value or throws an exception
public async Task<Result<Order>> ProcessOrderAsync(OrderRequest request)
{
return await ValidateOrderRequest(request)
.ThenAsync(async validRequest => await CreateOrder(validRequest))
.ThenAsync(async order => await ApplyDiscounts(order))
.ThenAsync(async order => await CalculateTaxes(order))
.ThenAsync(async order => await ChargePayment(order))
.ThenAsync(async order => await SendConfirmationEmail(order))
.ThenAsync(async order =>
{
await LogOrderProcessed(order);
return Result<Order>.Ok(order, "Order processed successfully");
});
}
// Usage
var result = await ProcessOrderAsync(orderRequest);
if (result.Success)
{
return Ok(result.Value);
}
else
{
_logger.LogError(result.Exception, "Order processing failed: {Message}", result.Message);
return BadRequest(result.Message);
}[HttpGet("{id}")]
public async Task<IActionResult> GetUser(int id)
{
var result = await _userService.GetUserAsync(id);
return result.Success
? Ok(result.Value)
: NotFound(result.Message);
}
[HttpPost]
public async Task<IActionResult> CreateUser([FromBody] CreateUserRequest request)
{
var validationResult = ValidateRequest(request);
if (!validationResult.Success)
{
return BadRequest(validationResult.Message);
}
var result = await _userService.CreateUserAsync(request);
return result.Success
? CreatedAtAction(nameof(GetUser), new { id = result.Value.Id }, result.Value)
: BadRequest(result.Message);
}// Centralized error handling
public Result<T> ExecuteWithErrorHandling<T>(Func<T> operation, string operationName)
{
try
{
var result = operation();
return Result<T>.Ok(result, $"{operationName} completed successfully");
}
catch (ValidationException ex)
{
return Result<T>.Fail($"Validation failed in {operationName}: {ex.Message}", ex);
}
catch (NotFoundException ex)
{
return Result<T>.Fail($"Resource not found in {operationName}: {ex.Message}", ex);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error in {OperationName}", operationName);
return Result<T>.Fail($"An unexpected error occurred in {operationName}", ex);
}
}- Use Result for Expected Failures - Reserve exceptions for truly exceptional cases
- Chain Operations - Leverage
ThenandThenAsyncfor clean, readable code - Avoid Nested Results - Use
Theninstead of manual checking - Consistent Error Messages - Provide clear, actionable error messages
- Leverage Implicit Conversions - Simplify code with implicit conversions where appropriate
- Prefer TryGetValue - Use
TryGetValueoverGetValueOrThrowfor safer value extraction - Aggregate Validation Errors - Use
Result.Combinefor multiple validation checks
We welcome contributions! Please see our Contributing Guide for details.
This project is licensed under the MIT License - see the LICENSE file for details.
- π§ Email: support@tethys.dev
- π Issues: GitHub Issues
- π Documentation: Full Documentation
Inspired by functional programming patterns and the Railway Oriented Programming approach.