RESTCLIENT.NET / DOCUMENTATION
Result Type
A discriminated union representing either a successful result or a failure. This type enforces explicit error handling and eliminates the need for exceptions in business logic.
Namespace: Outcome
Overview
The Result type follows the Railway Oriented Programming pattern. Every operation returns either:
Ok<TSuccess>- The "happy path" containing the success valueError<TFailure>- The "sad path" containing the error value
public abstract partial record Result<TSuccess, TFailure>
Nested Types
Ok<TSuccess, TFailure>
Represents a successful computation.
public sealed record Ok<TSuccess, TFailure>(TSuccess Value)
: Result<TSuccess, TFailure>
Error<TSuccess, TFailure>
Represents a failed computation.
public sealed record Error<TSuccess, TFailure>(TFailure Value)
: Result<TSuccess, TFailure>
Properties
IsOk
Returns true if this result represents a successful computation.
public bool IsOk { get; }
IsError
Returns true if this result represents a failed computation.
public bool IsError { get; }
Example
if (result.IsOk)
{
// Handle success
}
else
{
// Handle error
}
Static Methods
Failure
Creates a failed result containing the specified error.
public static Result<TSuccess, TFailure> Failure(TFailure error)
Example
Result<User, string> result = Result<User, string>.Failure("User not found");
Instance Methods
Map<TNewSuccess>
Transforms the success value using the provided function. If this is an Error, the error is propagated unchanged.
public Result<TNewSuccess, TFailure> Map<TNewSuccess>(
Func<TSuccess, TNewSuccess> mapper
)
This is the fundamental Functor operation for Result.
Example
Result<int, string> numberResult = GetNumber(); // Returns Ok(42)
Result<string, string> stringResult = numberResult.Map(n => n.ToString());
// stringResult is Ok("42")
Result<int, string> errorResult = Result<int, string>.Failure("Error");
Result<string, string> mappedError = errorResult.Map(n => n.ToString());
// mappedError is Error("Error") - map function is not called
MapError<TNewFailure>
Transforms the error value using the provided function. If this is an Ok, the success value is propagated unchanged.
public Result<TSuccess, TNewFailure> MapError<TNewFailure>(
Func<TFailure, TNewFailure> mapper
)
Example
Result<User, string> result = GetUser(); // Returns Error("Not found")
Result<User, CustomError> mappedResult = result.MapError(msg => new CustomError(msg));
// mappedResult is Error(CustomError("Not found"))
Bind<TNewSuccess>
Monadic bind operation. Applies a function that returns a Result to the success value, flattening the nested Result.
public Result<TNewSuccess, TFailure> Bind<TNewSuccess>(
Func<TSuccess, Result<TNewSuccess, TFailure>> binder
)
This is the core operation that makes Result a monad, enabling Railway Oriented Programming.
Example
Result<int, string> Validate(string input)
{
return int.TryParse(input, out var number)
? new Result<int, string>.Ok<int, string>(number)
: Result<int, string>.Failure("Invalid number");
}
Result<int, string> MultiplyByTwo(int n)
{
return new Result<int, string>.Ok<int, string>(n * 2);
}
Result<int, string> result = Validate("21").Bind(MultiplyByTwo);
// result is Ok(42)
Result<int, string> errorResult = Validate("abc").Bind(MultiplyByTwo);
// errorResult is Error("Invalid number") - MultiplyByTwo is not called
Match<TResult>
Applies one of two functions based on the state of this Result. This is the catamorphism for Result.
public TResult Match<TResult>(
Func<TSuccess, TResult> onSuccess,
Func<TFailure, TResult> onError
)
Example
string message = result.Match(
onSuccess: user => $"Hello, {user.Name}!",
onError: error => $"Error: {error}"
);
Tap
Performs a side effect based on the state without changing the Result.
public Result<TSuccess, TFailure> Tap(
Action<TSuccess>? onSuccess = null,
Action<TFailure>? onError = null
)
Use this for logging, debugging, or other side effects while preserving the functional chain.
Example
var result = GetUser()
.Tap(
onSuccess: user => logger.LogInformation("Found user: {Name}", user.Name),
onError: error => logger.LogWarning("User lookup failed: {Error}", error)
)
.Map(user => user.Email);
GetValueOrDefault
Returns the success value if Ok, otherwise returns the provided default.
public TSuccess GetValueOrDefault(TSuccess defaultValue)
public TSuccess GetValueOrDefault(Func<TSuccess> defaultProvider)
Example
string name = result.GetValueOrDefault("Unknown User");
// Lazy evaluation
string lazyName = result.GetValueOrDefault(() => ExpensiveDefaultCalculation());
Operators
! (Logical Not)
Extracts the error value if Error, throws if Ok.
public static TFailure operator !(Result<TSuccess, TFailure> result)
Example
Result<User, string> result = Result<User, string>.Failure("Not found");
string error = !result; // "Not found"
+ (Unary Plus)
Extracts the success value if Ok, throws if Error. Use only in tests.
public static TSuccess operator +(Result<TSuccess, TFailure> result)
Example
// In tests only!
Result<User, string> result = GetUser();
User user = +result; // Throws if Error
Pattern Matching
The recommended way to handle Results is with pattern matching:
var output = result switch
{
Result<User, ApiError>.Ok<User, ApiError>(var user)
=> $"Found: {user.Name}",
Result<User, ApiError>.Error<User, ApiError>(var error)
=> $"Error: {error.Message}",
};
With Type Aliases
Define type aliases in GlobalUsings.cs for cleaner code:
global using OkUser = Outcome.Result<User, ApiError>
.Ok<User, ApiError>;
global using ErrorUser = Outcome.Result<User, ApiError>
.Error<User, ApiError>;
Then pattern match becomes:
var output = result switch
{
OkUser(var user) => $"Found: {user.Name}",
ErrorUser(var error) => $"Error: {error.Message}",
};
Railway Oriented Programming
Chain operations that may fail:
var result = await ValidateInput(input)
.Bind(ParseData)
.Bind(TransformData)
.Map(FormatOutput);
// If any step fails, subsequent steps are skipped
// and the first error is propagated
Best Practices
- Prefer pattern matching over
IsOk/IsErrorchecks - Use
Bindfor operations that return Results - Use
Mapfor pure transformations - Use
Tapfor side effects (logging, metrics) - Define type aliases for frequently used Result types
- Never use
+operator outside of tests