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.
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. |
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 LeftTransforming values
| Method | Operates on | Left passes through | Notes |
|---|---|---|---|
map(mapper) | Right | Yes | Standard right-biased functor map. |
mapLeft(mapper) | Left | Yes (Right) | Transforms the left value; Right passes through. |
flatMap(mapper) | Right | Yes | Monadic bind for the right track. |
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"));
// 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.
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. |
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(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 → EitherEither<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 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