Whetstone.
The Fusion domain kitResult<T>: errors as values
Module 8 · Lesson 115 min

Result<T>: errors as values

Fusion domain code does not throw for validation. It returns a Result<T>: either a success carrying a value, or a failure carrying a list of EjError. Exceptions are reserved for the genuinely unexpected (a bug, a torn network). A flight number that’s blank isn’t exceptional; it’s an expected invalid input, so it comes back as a failed Result, not a thrown exception.

The Result primitives (real domain/Result.cs)
public record EjError(string Field, string Message);
public sealed record EntityNotFound(string Field, string Message) : EjError(Field, Message);

public sealed class Result<T>
{
    public bool IsSuccess { get; }
    public bool IsFailure => !IsSuccess;
    public T Value { get; }                     // throws if you read it on a failure
    public IReadOnlyList<EjError> Errors { get; }
    public static Result<T> Success(T value);
    public static Result<T> Failure(params EjError[] errors);
}

The building idiom is a fluent guard chain. FailIf(condition, error) starts a builder; you chain more FailIf calls; OrSuccess(...) builds the value only if nothing failed. The important detail: FailIf accumulates. It doesn’t stop at the first broken rule; it collects them all, so the caller gets every problem at once instead of fixing them one round-trip at a time.

The FailIf / OrSuccess chain (real DisruptionDelayVo)
public static Result<DisruptionDelayVo> Create(bool isCancellation, int? delayInMinutes) =>
    Result<DisruptionDelayVo>
        .FailIf(isCancellation && delayInMinutes is not null,
            new EjError(nameof(DelayInMinutes), "Delay must be null for a cancellation."))
        .FailIf(!isCancellation && delayInMinutes is null,
            new EjError(nameof(DelayInMinutes), "Delay is required when not a cancellation."))
        .OrSuccess(() => new DisruptionDelayVo(isCancellation, delayInMinutes));
Practice

Try it yourself

Do

Trace an accumulating validation

Prove to yourself that FailIf collects, not short-circuits.

  • Open domain/ValueObjects/DisruptionDelayVo.cs and read the Create chain
  • In a scratch test, call DisruptionDelayVo.Create(isCancellation: false, delayInMinutes: null) and assert the result is a failure
  • Confirm you can read result.Errors and the field is DelayInMinutes
  • Note that a use case would return these errors straight to the caller rather than throwing
Done whenAn invalid Create call returned a failed Result whose Errors you inspected, with no exception thrown.
Quiz

Throw or Result

A caller passes a blank flight number to a domain factory. What should the factory do?

  1. AThrow an ArgumentException, since that is exactly what exceptions are for
  2. BReturn null and let the caller figure it out
  3. CReturn Result.Failure with an EjError; a blank input is expected invalid data, not exceptional
  4. DLog a warning and use a default flight number
Show answer

Correct answer: C — Return Result.Failure with an EjError; a blank input is expected invalid data, not exceptional

Blank input is an expected validation failure, so it returns a failed Result carrying an EjError (anti-pattern #8). Exceptions are reserved for the genuinely unexpected. Never silently null or default.

Sign in to track your progress →