Either<L, R>
Runnable example:
EitherSample.java
What is Either<L, R>?
Either<L, R> represents a value that is one of exactly two types.
It is a sealed interface with two implementations:
Left<L, R>— holds a non-null value of typeL.Right<L, R>— holds a non-null value of typeR.
The critical distinction from Result<V, E> is that Either carries no error
semantics. Neither side means success or failure — both are legitimate outcomes.
Use Either when a computation can return one of two unrelated value types and
neither should be labelled an error.
By convention, map, flatMap, and related operations act on the right side.
Use swap() to flip the sides when you need to operate on the left.
This design — semantically neutral but right-biased for operations — is documented in ADR-007 — Either as a neutral type with no directional bias.
When to use Either vs other types:
| Scenario | Use |
|---|---|
| One track is an error, the other is success | Result<V, E> |
| Absent vs present value | Option<T> |
| Wrapping throwing code | Try<V> |
| Collecting all validation errors | Validated<E, A> |
| Two equally valid outcomes with different types | Either<L, R> |
Creating instances
// Either carries no error semantics — both sides are equal citizensEither<String, Integer> right = Either.right(42);Either<String, Integer> left = Either.left("admin");
// Typical use: a computation that returns one of two unrelated typesEither<AdminUser, RegularUser> user = authenticate(token);
// From conversions — other types can produce an EitherEither<Throwable, String> fromTry = Try.of(() -> fetch(url)).toEither();Either<String, Integer> fromResult = Result.<Integer, String>ok(1).toEither();Either<String, Integer> fromValidated = Validated.<String, Integer>valid(1).toEither();Either<String, Option<Integer>> fromOption = Either.right(Option.some(42));Both Left and Right reject null. Most production code obtains an
Either via a conversion from another type rather than constructing it directly.
Checking state
Either<String, Integer> e = authenticate(token);
e.isLeft(); // true if Lefte.isRight(); // true if Right
// Exhaustive pattern matching — preferredString msg = switch (e) { case Either.Left<String, Integer> l -> "Admin: " + l.value(); case Either.Right<String, Integer> r -> "Regular: " + r.value();};Prefer exhaustive switch patterns — the compiler enforces that both sides are handled.
Extracting values
| Method | On wrong side | Notes |
|---|---|---|
getLeft() | Throws NoSuchElementException | Only safe after an isLeft() check. |
getRight() | Throws NoSuchElementException | Only safe after an isRight() check. |
getLeftOrElse(fallback) | Returns fallback | Fallback is always evaluated — use getLeftOrElseGet for expensive defaults. |
getRightOrElse(fallback) | Returns fallback | Fallback is always evaluated — use getRightOrElseGet for expensive defaults. |
getLeftOrElseGet(supplier) | Calls supplier | Lazily computed fallback for the left track. |
getRightOrElseGet(supplier) | Calls supplier | Lazily computed fallback for the right track. |
fold(onLeft, onRight) | N/A — handles both sides | The safest extractor; forces both branches. |
Either<String, Integer> e = compute();
// fold — maps both sides to a common type; forces both to be handledString rendered = e.fold( left -> "Left: " + left, right -> "Right: " + right);
// Unsafe accessors — throw NoSuchElementException on the wrong sideString leftVal = e.getLeft(); // throws if Rightint rightVal = e.getRight(); // throws if Left
// Safe accessors with a fallback value (eagerly evaluated)int rightOrZero = e.getRightOrElse(0);String leftOrDef = e.getLeftOrElse("default");
// Safe accessors with a lazy supplier (supplier called only if wrong side)int rightLazy = e.getRightOrElseGet(() -> expensiveDefault());String leftLazy = e.getLeftOrElseGet(() -> computeDefault());Transforming values
| Method | Operates on | Passes through | Notes |
|---|---|---|---|
map(mapper) | Right | Left | Standard right-biased functor map. Mapper must not return null. |
mapLeft(mapper) | Left | Right | Transforms the left value. Mapper must not return null. |
flatMap(mapper) | Right | Left | Monadic bind for the right track. |
flatMapLeft(mapper) | Left | Right | Monadic bind for the left track; symmetric with flatMap. |
swap() | Both | N/A | Flips Left ↔ Right; useful before operating on Left. |
fold(fn, fn) | Both | N/A | Terminal — collapses both sides into one value. |
Either<String, Integer> e = Either.right(21);
// map — transforms the right value; Left passes through unchangedEither<String, String> hex = e.map(Integer::toHexString); // Right("15")
// mapLeft — transforms the left value; Right passes through unchangedEither<Integer, Integer> e2 = Either.<String, Integer>left("error") .mapLeft(String::length); // Left(5)
// flatMap — chains right-biased operationsEither<String, String> result = Either.<String, Integer>right(42) .flatMap(n -> n > 0 ? Either.right(Integer.toBinaryString(n)) : Either.left("non-positive"));
// flatMapLeft — chains left-biased operations (symmetric with flatMap)Either<String, Integer> recovered = Either.<String, Integer>left("retry") .flatMapLeft(s -> s.equals("retry") ? Either.right(0) : Either.left("permanent failure"));
// swap — flips Left ↔ RightEither<Integer, String> swapped = Either.<String, Integer>left("hello").swap();// → Right("hello")Side effects
Either<String, Integer> e = resolve(input);
// peek — runs action on Right; returns this for chaininge.peek(n -> log.info("Resolved: {}", n)) .peekLeft(s -> log.warn("Fallback branch: {}", s));peek runs an action on the Right value and returns this unchanged.
peekLeft does the same for the Left value. Both are chainable.
match(onLeft, onRight) is the terminal, side-effecting counterpart to fold:
it executes exactly one consumer and returns void.
Interoperability
| Conversion | Method / Factory | Notes |
|---|---|---|
Either → Option | e.toOption() | Right → Some; Left → None (discarded). |
Either → Result | e.toResult() | Right → Ok; Left → Err. |
Either → Validated | e.toValidated() | Right → Valid; Left → Invalid. |
Either → Optional | e.toOptional() | Right(r) → Optional.of(r); Left → Optional.empty(). |
Either → Try | e.toTry(leftMapper) | Right → Success; Left mapped to Throwable → Failure. |
Either → Stream (right) | e.stream() | Right(r) → Stream.of(r); Left → Stream.empty(). |
Either → Stream (left) | e.streamLeft() | Left(l) → Stream.of(l); Right → Stream.empty(). |
Try → Either | tryVal.toEither() | Success → Right; Failure → Left. |
Result → Either | result.toEither() | Ok → Right; Err → Left. |
Validated → Either | validated.toEither() | Valid → Right; Invalid → Left. |
Option → Either | option.toEither(leftIfNone) | Some → Right; None → Left(leftIfNone). |
Either<String, Integer> e = compute();
// ── Either → other types ────────────────────────────────────────────────────
// → Option (Right → Some, Left → None — left value discarded)Option<Integer> opt = e.toOption();
// → Result (Right → Ok, Left → Err)Result<Integer, String> result = e.toResult();
// → Validated (Right → Valid, Left → Invalid)Validated<String, Integer> validated = e.toValidated();
// → Optional (Right(r) → Optional.of(r), Left → Optional.empty())Optional<Integer> optional = e.toOptional();
// → Try (Right → Success, Left mapped to Throwable → Failure)Try<Integer> tried = e.toTry(IllegalArgumentException::new);
// → Stream (Right(r) → Stream.of(r), Left → Stream.empty())// Useful for flatMap pipelines that collect only the right valuesList<Integer> rights = Stream.of(Either.right(1), Either.left("err"), Either.right(3)) .flatMap(Either::stream) .toList(); // [1, 3]
// → Stream of left (Left(l) → Stream.of(l), Right → Stream.empty())// Symmetric with stream(); collects only the left valuesList<String> lefts = Stream.of(Either.right(1), Either.left("err"), Either.left("bad")) .flatMap(Either::streamLeft) .toList(); // ["err", "bad"]
// ── Other types → Either ────────────────────────────────────────────────────
// Try → Either (Success → Right, Failure → Left with the exception)Either<Throwable, String> fromTry = Try.of(() -> fetch()).toEither();
// Result → Either (Ok → Right, Err → Left)Either<String, Integer> fromResult = Result.<Integer, String>ok(1).toEither();
// Validated → Either (Valid → Right, Invalid → Left)Either<String, Integer> fromValidated = Validated.<String, Integer>valid(1).toEither();
// Option → Either (Some(v) → Right(v), None → Left(leftIfNone))Either<String, Integer> fromOption = Option.some(42).toEither("value absent");See the Combining Types page for the full conversion matrix and composition patterns.
Real-world example
Route dispatch where authenticated users are either admins or regular users —
neither outcome is an error. Either makes the two-track branching explicit
without implying that one side is a failure.
// Route dispatch: an authenticated request is either an admin or a regular user.// Neither side is an "error" — both are valid outcomes with different behaviour.
Either<AdminUser, RegularUser> principal = authenticate(request.token());
// Right-biased pipeline on the regular-user trackResponse response = principal .map(user -> enrichWithPreferences(user)) // only for RegularUser .fold( admin -> adminDashboard(admin), regular -> userDashboard(regular) );
// swap + flatMap to operate on the left trackEither<AdminReport, RegularUser> report = principal .swap() // AdminUser is now Right .flatMap(admin -> Either.right(buildReport(admin))) .swap(); // restore original orientation