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:

ScenarioUse
One track is an error, the other is successResult<V, E>
Absent vs present valueOption<T>
Wrapping throwing codeTry<V>
Collecting all validation errorsValidated<E, A>
Two equally valid outcomes with different typesEither<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

MethodOn wrong sideNotes
getLeft()Throws NoSuchElementExceptionOnly safe after an isLeft() check.
getRight()Throws NoSuchElementExceptionOnly safe after an isRight() check.
getLeftOrElse(fallback)Returns fallbackFallback is always evaluated — use getLeftOrElseGet for expensive defaults.
getRightOrElse(fallback)Returns fallbackFallback is always evaluated — use getRightOrElseGet for expensive defaults.
getLeftOrElseGet(supplier)Calls supplierLazily computed fallback for the left track.
getRightOrElseGet(supplier)Calls supplierLazily computed fallback for the right track.
fold(onLeft, onRight)N/A — handles both sidesThe 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

MethodOperates onPasses throughNotes
map(mapper)RightLeftStandard right-biased functor map. Mapper must not return null.
mapLeft(mapper)LeftRightTransforms the left value. Mapper must not return null.
flatMap(mapper)RightLeftMonadic bind for the right track.
flatMapLeft(mapper)LeftRightMonadic bind for the left track; symmetric with flatMap.
swap()BothN/AFlips Left ↔ Right; useful before operating on Left.
fold(fn, fn)BothN/ATerminal — 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

ConversionMethod / FactoryNotes
EitherOptione.toOption()Right → Some; Left → None (discarded).
EitherResulte.toResult()Right → Ok; Left → Err.
EitherValidatede.toValidated()Right → Valid; Left → Invalid.
EitherOptionale.toOptional()Right(r) → Optional.of(r); Left → Optional.empty().
EitherTrye.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().
TryEithertryVal.toEither()Success → Right; Failure → Left.
ResultEitherresult.toEither()Ok → Right; Err → Left.
ValidatedEithervalidated.toEither()Valid → Right; Invalid → Left.
OptionEitheroption.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