Field Notes

Scala/Play, Part 12: Scala Either for Java Developers - A Result or a Reason

Scala/Play, Part 12: Scala Either for Java Developers - A Result or a Reason

Coming from Java and Spring, I find Scala easier to read when I expand the short version first. That worked for implicit classes. For Either, the useful starting point was the policy lookup I already had: sometimes it returns a policy, sometimes it returns nothing. Now I want an explanation to travel with the unsuccessful result.

This is plain Scala 2.13.18, with fictional insurance data and no Play dependencies. The runnable lesson builds the idea through pattern matching, map, flatMap, and a for-comprehension.

Zakaria’s navy-hoodie mascot at a parcel depot, holding an envelope beside a tray with an explanatory warning note.
A useful result or an explanation: the illustration sets the theme; the code below defines the exact behavior.

When absence needs a reason

Option[PolicySnapshot] answers whether a policy exists. Some(policy) carries the policy; None carries no payload. That remains a good model when absence is all the caller needs. For this next example, I want either a policy or a message describing why the lookup did not produce one.

Either[String, PolicySnapshot]

The first type argument is the payload of Left; the second is the payload of Right. The result contains one alternative, not both. For error handling, I use the convention Left = failure and Right = success. Either itself is more general than exceptions, and the left payload does not have to be a throwable.

val found: Either[String, PolicySnapshot] =
  Right(PolicySnapshot("POL-001", 600, 3))

val missing: Either[String, PolicySnapshot] =
  Left("Policy not found: POL-999")

I keep the existing lookup and explicitly choose what to return for each outcome:

def findPolicy(number: String): Either[String, PolicySnapshot] = {
  OptionLesson.findPolicy(number) match {
    case Some(policy) => Right(policy)
    case None => Left("Policy not found: " + number)
  }
}

OptionLesson.findPolicy is the earlier one-policy fixture: it knows POL-001 and returns None otherwise. The new message comes from my code, not hidden information recovered from None. A returned Left is ordinary data: it does not throw, and declaring an Either return type does not automatically catch exceptions.

Read both alternatives with match

def describePolicy(result: Either[String, PolicySnapshot]): String = {
  result match {
    case Right(policy) => "Found policy " + policy.number
    case Left(reason) => "Cannot continue: " + reason
  }
}

These are patterns, just like Some(policy). In the first branch, policy is a PolicySnapshot; in the second, reason is a String. Both branches produce the method’s final String. This keeps an expected failure visible in the return value rather than requiring an exception merely to carry a message.

map transforms the successful value

To extract the policy number while preserving a failure, I can write both branches explicitly:

def policyNumberWithMatch(
    result: Either[String, PolicySnapshot]
): Either[String, String] = {
  result match {
    case Right(policy) => Right(policy.number)
    case Left(reason) => Left(reason)
  }
}

Or express only the success transformation:

def policyNumberWithMap(
    result: Either[String, PolicySnapshot]
): Either[String, String] = {
  result.map((policy: PolicySnapshot) => policy.number)
}

The lambda receives a policy and returns a plain String. map supplies the Right wrapper; on a Left, it skips the lambda and retains the reason. The result type becomes Either[String, String]: the first String is still the error, while the second is now the policy number. The original policy result is unchanged.

This resembles Java’s Optional.map, with an important extra payload on the unsuccessful side. Scala 2.13 calls Either right-biased: these operations act on Right. The version-specific standard-library documentation confirms that behavior and its use in for-comprehensions.

When the next operation can fail too

Extracting a number returns a String. Looking up a contact email can fail, so that operation returns another Either:

def findContactEmail(policy: PolicySnapshot): Either[String, String] = {
  if (policy.number == "POL-001") Right("customer@example.com")
  else Left("Contact email not found: " + policy.number)
}

Using map here is legal, but its result is nested:

def emailWithMap(
    result: Either[String, PolicySnapshot]
): Either[String, Either[String, String]] = {
  result.map((policy: PolicySnapshot) => findContactEmail(policy))
}

If both steps succeed, that gives Right(Right("customer@example.com")). If the policy exists but has no email, it gives Right(Left("Contact email not found: POL-002")). The outer Right only tells me the first step succeeded. It is not a claim that the email exists.

Comparison of nested map results and flatMap results for successful lookups, missing email, and missing policy.
The callback already returns Either. map wraps that result; flatMap uses it directly.

The explicit behavior I want is to return the second lookup’s result directly:

def emailWithMatch(
    result: Either[String, PolicySnapshot]
): Either[String, String] = {
  result match {
    case Right(policy) => findContactEmail(policy)
    case Left(reason) => Left(reason)
  }
}

That is precisely the role of flatMap:

def emailWithFlatMap(
    result: Either[String, PolicySnapshot]
): Either[String, String] = {
  result.flatMap((policy: PolicySnapshot) => findContactEmail(policy))
}

Now success is Right(email), a missing contact is Left(contactReason), and a missing policy remains Left(policyReason). In the last case, the contact lookup is never called. The test for an existing policy without email uses a direct Right(PolicySnapshot("POL-002", 600, 2)) fixture; the original lookup has not magically learned a second policy.

The same chain, written with for

To build a label containing both the policy number and email, I use an outer flatMap and an inner map:

def contactLabelWithMethods(
    result: Either[String, PolicySnapshot]
): Either[String, String] = {
  result.flatMap((policy: PolicySnapshot) => {
    findContactEmail(policy).map((email: String) => {
      policy.number + " -> " + email
    })
  })
}

The for-comprehension expresses the same operations:

def contactLabelWithFor(
    result: Either[String, PolicySnapshot]
): Either[String, String] = {
  for {
    policy <- result
    email <- findContactEmail(policy)
  } yield policy.number + " -> " + email
}

After the first generator, policy is a PolicySnapshot. After the second, email is a String. The expression after yield builds a plain String; the final map wraps it in Right. Writing yield Right(...) would add an unwanted layer. Either Left skips the label construction and preserves its reason.

An Either for-comprehension next to its equivalent flatMap and map calls, with the bound types and final result type.
This is syntax for the same dependent chain, not a collection loop or a try/catch.

Run all three paths

From the lab root:

sbt "runMain learning.PremiumLesson"
For, both found: Right(POL-001 -> customer@example.com)
For, no email: Left(Contact email not found: POL-002)
For, no policy: Left(Policy not found: POL-999)

The lesson asserts these exact results and equivalence with the method-chain version. Earlier checks verify that map and flatMap skip their callbacks on an initial Left. Everything runs with the existing lab assertions.

My rule of thumb is now concrete: use map when the function produces the next value, and flatMap when it already produces the next Either. Use a for-comprehension when the dependent steps are easier to read top to bottom. I have not replaced every exception or optional value; I have made this particular sequence return a useful result or a useful reason.