Result and Either: errors as values
Option handles absence, but absence is not enough for a registry lookup. “Citizen not found,” “registry timed out,” and “identity document invalid” are different failures with different responses — a retry, a rejection, a manual-review routing. Result<T, E> carries a success value of type T or a typed error of type E, and expected failures stop hiding in throws clauses the compiler cannot see.
The error type first
Scheme engines talk to registries that fail in a handful of known ways. Model them as the sealed type from Part 4’s playbook:
public sealed interface RegistryError {
record NotFound(String citizenId) implements RegistryError {}
record Timeout(String service) implements RegistryError {}
record Unavailable(String service) implements RegistryError {}
record InvalidIdentity(String detail) implements RegistryError {}}Now Result<CitizenFacts, RegistryError> says exactly what a lookup can produce — success, or one of four named failures.
The interface
public sealed interface Result<T, E> {
<R> Result<R, E> map(Fn1<? super T, ? extends R> mapper);
<F> Result<T, F> mapError(Fn1<? super E, ? extends F> mapper);
<R> Result<R, E> flatMap(Fn1<? super T, ? extends Result<R, E>> mapper);
Result<T, E> recover(Fn1<? super E, ? extends T> recovery);
<R> R fold(Fn1<? super E, ? extends R> onFailure, Fn1<? super T, ? extends R> onSuccess);
static <T, E> Result<T, E> success(T value) { return new Success<>(value); }
static <T, E> Result<T, E> failure(E error) { return new Failure<>(error); }
record Success<T, E>(T value) implements Result<T, E> { /* ... */ } record Failure<T, E>(E error) implements Result<T, E> { /* ... */ }}Success implements the value-side operations the way Some does — map applies the function, flatMap returns the mapper’s result. Failure is the interesting one: map and flatMap pass the error through untouched, mapError transforms it, and recover converts it back to a success. The asymmetry is the point — the error channel is preserved through every transformation until something explicitly handles it.
At the registry boundary
public interface CitizenRegistry {
Result<CitizenFacts, RegistryError> loadCitizen(String citizenId);}The adapter wraps the real HTTP call: timeouts become Failure(new Timeout("citizen-registry")), a 404 becomes Failure(new NotFound(citizenId)), a parseable response becomes Success(facts). Inside the pipeline, failures compose like values — and at the HTTP edge, fold plus an exhaustive switch turns the typed error into a status code, never a catch-all Exception handler:
HttpResponse response = registry.loadCitizen(citizenId).fold( error -> switch (error) { case RegistryError.NotFound(var id) -> HttpResponse.notFound("unknown citizen " + id); case RegistryError.Timeout(var svc) -> HttpResponse.serviceUnavailable(svc + " timed out"); case RegistryError.Unavailable(var svc) -> HttpResponse.serviceUnavailable(svc + " is down"); case RegistryError.InvalidIdentity(var detail) -> HttpResponse.unprocessable(detail); }, facts -> HttpResponse.ok(facts));HttpResponse stands in for whatever response type your framework hands you — the substance is the switch. Because RegistryError is sealed, adding a fifth error variant breaks every switch that forgot it: the compiler becomes your audit checklist.
Either, honestly
Either<L, R> is Result without the success/failure connotation — a value that is Left or Right where neither is privileged. jfp implements it (data/Either.java, same shape: map, mapLeft, flatMap, fold) because some integrations genuinely have two equally valid outcomes. In practice Result covers almost every case you will meet; Either earns its place in the library mostly as the concept that teaches you the bias: Result.map works on the right side by convention, and Either makes you notice that convention was a choice.
Compared with the JDK: Java has no Result. The nearest analogs are Optional (no error channel), exceptions (invisible channel), and CompletableFuture’s exceptional completion (async only). For expected, named failures in synchronous code, a Result type is the honest representation — which is why Kotlin’s Result, Rust’s, and Scala’s Either/Try all exist.