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 type L.
  • Right<L, R> — holds a non-null value of type R.

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 citizens
Either<String, Integer> right = Either.right(42);
Either<String, Integer> left = Either.left("admin");
// Typical use: a computation that returns one of two unrelated types
Either<AdminUser, RegularUser> user = authenticate(token);
// From conversions — other types can produce an Either
Either<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 Left
e.isRight(); // true if Right
// Exhaustive pattern matching — preferred
String 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 handled
String rendered = e.fold(
left -> "Left: " + left,
right -> "Right: " + right
);
// Unsafe accessors — throw NoSuchElementException on the wrong side
String leftVal = e.getLeft(); // throws if Right
int 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 unchanged
Either<String, String> hex = e.map(Integer::toHexString); // Right("15")
// mapLeft — transforms the left value; Right passes through unchanged
Either<Integer, Integer> e2 = Either.<String, Integer>left("error")
.mapLeft(String::length); // Left(5)
// flatMap — chains right-biased operations
Either<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 ↔ Right
Either<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 chaining
e.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
EitherOption e.toOption() Right → Some; Left → None (discarded).
EitherResult e.toResult() Right → Ok; Left → Err.
EitherValidated e.toValidated() Right → Valid; Left → Invalid.
EitherOptional e.toOptional() Right(r) → Optional.of(r); Left → Optional.empty().
EitherTry e.toTry(leftMapper) Right → Success; Left mapped to Throwable → Failure.
EitherStream (right) e.stream() Right(r) → Stream.of(r); Left → Stream.empty().
EitherStream (left) e.streamLeft() Left(l) → Stream.of(l); Right → Stream.empty().
TryEither tryVal.toEither() Success → Right; Failure → Left.
ResultEither result.toEither() Ok → Right; Err → Left.
ValidatedEither validated.toEither() Valid → Right; Invalid → Left.
OptionEither 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 values
List<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 values
List<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 track
Response response = principal
.map(user -> enrichWithPreferences(user)) // only for RegularUser
.fold(
admin -> adminDashboard(admin),
regular -> userDashboard(regular)
);
// swap + flatMap to operate on the left track
Either<AdminReport, RegularUser> report = principal
.swap() // AdminUser is now Right
.flatMap(admin -> Either.right(buildReport(admin)))
.swap(); // restore original orientation