Result and EjError
Here is the single move that most separates Fusion code from slop. Domain validation never throws. It returns a Result<T>: either a success carrying a value, or a failure carrying a list of errors. The caller inspects the Result and decides what to do. No try/catch for expected, everyday validation failures.
An error is a small record. EjError has a Field (which part failed) and a Message (why). There are typed subtypes so a caller can branch on the KIND of failure, not on the message text.
public record EjError(string Field, string Message);
public sealed record EntityNotFound(string Field, string Message) : EjError(Field, Message);
public sealed record EjTimeout(string Field, string Message) : EjError(Field, Message);Result<T> itself gives you IsSuccess / IsFailure, the Errors list, and a Value. Reading Value on a failed result throws, on purpose: it’s a programming error to look at a value that isn’t there, so it fails loudly rather than handing you a default. You construct results with the static Success and Failure factories.
public bool IsSuccess { get; }
public bool IsFailure => !IsSuccess;
public T Value => IsSuccess
? _value!
: throw new InvalidOperationException("Cannot access Value on a failed Result. Check IsSuccess first.");
public static Result<T> Success(T value) => new(value);
public static Result<T> Failure(params EjError[] errors) => /* ... */;Try it yourself
Read a Result correctly
Wire your fingers to check before you access. Sketch this (in a scratch file or on paper) against the real API above.
- Given a
Result<FlightNoteVo> result, write the guard that returns early on failure:if (result.IsFailure) return ...; - Only after that guard, access
result.Valueand use it - Convince yourself why reading
result.Valuebefore the guard is a bug the type deliberately punishes (it throws)
IsFailure (or IsSuccess) first and only touch Value on the success path, because Value throws on a failed Result.Type over message
This is a named anti-pattern; have it cold.
An endpoint needs to return 404 for a missing flight and 504 for a timeout. What does it branch on, and what must it never branch on?
Reveal answer
It branches on the error TYPE (EntityNotFound, EjTimeout), pattern-matching the subtype. It must never branch on the error message string or a domain status string. Strings are untyped and brittle (anti-patterns #2 and #26).