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.
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.
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));Try it yourself
Trace an accumulating validation
Prove to yourself that FailIf collects, not short-circuits.
- Open
domain/ValueObjects/DisruptionDelayVo.csand read theCreatechain - In a scratch test, call
DisruptionDelayVo.Create(isCancellation: false, delayInMinutes: null)and assert the result is a failure - Confirm you can read
result.Errorsand the field isDelayInMinutes - Note that a use case would return these errors straight to the caller rather than throwing
Throw or Result
A caller passes a blank flight number to a domain factory. What should the factory do?
Show answer
Correct answer: C — Return Result
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.