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.

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.
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

Transforming values

MethodOperates onLeft passes throughNotes
map(mapper)RightYesStandard right-biased functor map.
mapLeft(mapper)LeftYes (Right)Transforms the left value; Right passes through.
flatMap(mapper)RightYesMonadic bind for the right track.
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"));
// 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.

Interoperability

ConversionMethod / FactoryNotes
EitherOptione.toOption()Right → Some; Left → None (discarded).
EitherResulte.toResult()Right → Ok; Left → Err.
EitherValidatede.toValidated()Right → Valid; Left → Invalid.
TryEithertryVal.toEither()Success → Right; Failure → Left.
ResultEitherresult.toEither()Ok → Right; Err → Left.
ValidatedEithervalidated.toEither()Valid → Right; Invalid → Left.
OptionEitheroption.toEither(leftSupplier)Some → Right; None → Left.
Either<String, Integer> e = compute();
// Either → Option (Right → Some, Left → None — left value discarded)
Option<Integer> opt = e.toOption();
// Either → Result (Right → Ok, Left → Err)
Result<Integer, String> result = e.toResult();
// Either → Validated (Right → Valid, Left → Invalid)
Validated<String, Integer> validated = e.toValidated();
// Other types → Either
Either<Throwable, String> fromTry = Try.of(() -> fetch()).toEither();
Either<String, Integer> fromResult = Result.<Integer, String>ok(1).toEither();
Either<String, Integer> fromValidated = Validated.<String, Integer>valid(1).toEither();
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