Result<T> Type
Result<T> is the core type of Spur. It represents an operation that either succeeds with a value of type T or fails with an Error.
Definition
public readonly struct Result<T>
{
public bool IsSuccess { get; }
public bool IsFailure { get; }
public T Value { get; } // throws SpurException if failed
public Error Error { get; } // throws SpurException if succeeded
}
Because it's a readonly struct:
- There are zero heap allocations on the success path.
- It's stack-allocated, so it's extremely fast.
- It has value semantics (equality is by value).
- It can never be
null.
Creating Results
Success
// Explicit factory
var result = Result.Success(42);
// Implicit conversion
Result<int> result = 42;
// Pipeline entry point
var result = Result.Start(42);
// Void operations (no meaningful value)
var result = Result.Success(); // returns Result<Unit>
Failure
// From an Error value
var error = Error.NotFound("User not found");
Result<User> result = error; // implicit conversion
// Explicit factory
var result = Result.Failure<User>(error);
// Inline
var result = Result.Failure<User>("NOT_FOUND", "User not found", 404);
Accessing the value
IsSuccess / IsFailure
var result = GetUser(123);
if (result.IsSuccess)
Console.WriteLine(result.Value.Name);
else
Console.WriteLine(result.Error.Message);
GetValueOrDefault
Returns default(T) when the Result is a failure:
var user = result.GetValueOrDefault(); // null if failed
UnwrapOr
Provide an explicit fallback:
var count = result.UnwrapOr(0); // 0 if failed
UnwrapOrElse
Compute a fallback from the error:
var user = result.UnwrapOrElse(err => User.Guest);
Unwrap
Returns the value or throws SpurException:
var user = result.Unwrap(); // throws if failed
Match
Branch on success/failure and produce a new value:
string message = result.Match(
onSuccess: user => $"Hello, {user.Name}",
onFailure: error => $"Error: {error.Message}"
);
MatchAsync
Async version of Match:
var html = await result.MatchAsync(
onSuccess: async user => await RenderProfileAsync(user),
onFailure: async error => await RenderErrorPageAsync(error)
);
Switch / SwitchAsync
Like Match, but performs actions without returning a value:
result.Switch(
onSuccess: user => _logger.LogInformation("Found {Name}", user.Name),
onFailure: error => _logger.LogWarning("Failed: {Code}", error.Code)
);
Implicit conversions
You can return values or errors directly — Spur converts them for you:
public Result<User> GetUser(int id)
{
var user = _db.Users.Find(id);
if (user is null)
return Error.NotFound("User not found"); // implicit Error → Result<User>
return user; // implicit User → Result<User>
}
Try / TryAsync
Wrap code that might throw into a Result automatically:
// Sync
var result = Result.Try(() => int.Parse(input));
// Async
var result = await Result.TryAsync(() => _http.GetAsync(url));
If the delegate throws, you get Error.Unexpected with the exception details.
Pipeline operators
The real power of Result<T> is chaining operations:
return await Result.Start(userId)
.ThenAsync(id => _repo.FindAsync(id))
.Validate(user => user.IsActive, Error.Validation("Inactive"))
.Map(user => new UserDto(user))
.TapError(err => _logger.LogError("{Code}: {Message}", err.Code, err.Message));
Each operator returns a new Result. If any step fails, the pipeline short-circuits.
See the Pipeline Operators section for details on each operator.
Combining Results
Combine (fail-fast)
Returns the first failure encountered:
var combined = Result.Combine(result1, result2, result3);
// combined.Value is IReadOnlyList<T> if all succeed
CombineAll (collect all errors)
Collects every failure into one aggregated error:
var combined = Result.CombineAll(result1, result2, result3);
Typed Combine
Combine Results of different types into a tuple:
var combined = Result.Combine(userResult, orderResult);
// combined.Value is (User, Order) if both succeed
Next steps
- Error type — how errors work
- Result-Oriented Programming — the big picture
- Pipeline operators — composing operations