Either & Try
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Scala 3.10 (3.10.0, released 2026-10-12). Every example was compiled and run with Scala 3.10.0
through Scala CLI on JDK 26 (checked on 2026-10-15). Either and Try are part of the standard library and have not
changed in years, so the 3.9 LTS line (3.9.0, 2026-09-03) and 3.3 LTS (3.3.8) behave the same, with one difference
in for comprehensions described in Scala 3.3 LTS versus 3.9 and 3.10. The one example that uses a third-party library (Cats) is
labelled as such and was run with the library fetched from Maven Central.
Option (see Option & Null Safety) says that a value may be
missing, but not why. When the reason matters, there are two standard types that carry it:
-
Either[E, A]holds either a failure of typeE(aLeft) or a success of typeA(aRight). You choose the type of the failure, so it can be an enum, a string, a case class or an exception. -
Try[A]holds either a success (Success(a)) or a failure (Failure(throwable)), and is built to wrap code that throws.
The examples use the trailhead scenario: a toolkit that plans hikes on a network of mountain trails, where reading a trail length from user input can fail in several ways.
Either: a value or a reason
Either[E, A] is a sealed abstract class with two final case classes, Left[E, A] and Right[E, A]. It is right-biased
since Scala 2.12: map, flatMap, filterOrElse, getOrElse, fold and for comprehensions all work on the Right value and
pass a Left through unchanged. By convention Left is the failure and Right is the success (the mnemonic is "right" as in
"correct"), but the type itself does not enforce it.
The failure type is whatever you declare. A good default is an enum of the failures a function can produce, because the
compiler then checks that a pattern match handles every case. See Enums and
Algebraic Data Types & GADTs.
//> using scala 3.10.0
package trailhead
enum TrailError:
case Blank
case NotANumber(text: String)
case TooLong(km: Double)
def parseKm(text: String): Either[TrailError, Double] =
if text.isBlank then Left(TrailError.Blank)
else text.toDoubleOption.toRight(TrailError.NotANumber(text))
def checkLength(km: Double): Either[TrailError, Double] =
if km > 30 then Left(TrailError.TooLong(km)) else Right(km)
def plan(text: String): Either[TrailError, String] =
for
km <- parseKm(text)
ok <- checkLength(km)
yield s"$ok km planned"
@main def run(): Unit =
println(plan("12.5"))
println(plan(""))
println(plan("abc"))
println(plan("42"))
Right(12.5 km planned)
Left(Blank)
Left(NotANumber(abc))
Left(TooLong(42.0))
plan chains two steps that can fail. The for comprehension runs parseKm; if it returns a Left, the comprehension
stops and produces that Left unchanged, and otherwise it passes the number to checkLength. The four calls show the
success case and the three failures, and the function’s signature tells every caller exactly which failures to expect.
Working with an Either
The commonly used operations, all shown in the program below:
| Method | What it does |
|---|---|
|
Transform or chain the |
|
Transforms the |
|
Exchanges the two sides. |
|
Collapses both cases to one result type. |
|
Fallback value, or fallback |
|
Turns a |
|
Tests on the |
|
Conversions (see below). |
|
Builds an |
//> using scala 3.10.0
package trailhead
def parseKm(text: String): Either[String, Double] =
text.toDoubleOption.toRight(s"not a number: $text")
@main def run(): Unit =
println(parseKm("3").map(_ * 2))
println(parseKm("x").map(_ * 2))
println(parseKm("x").left.map(e => s"error: $e"))
println(parseKm("x").swap)
println(parseKm("7").fold(e => s"failed with $e", km => s"ok $km"))
println(parseKm("7").getOrElse(0.0))
println(parseKm("q").getOrElse(0.0))
println(parseKm("q").orElse(Right(1.0)))
println(parseKm("7").toOption)
println(parseKm("q").toOption)
println(parseKm("7").exists(_ > 5))
println(parseKm("7").filterOrElse(_ > 10, "too short"))
println(parseKm("7").isRight)
println(parseKm("q").isLeft)
println(parseKm("7").contains(7.0))
println(parseKm("7").toSeq)
println(Either.cond(7 > 3, "long enough", "too short"))
println(Either.cond(1 > 3, "long enough", "too short"))
println(parseKm("q").left.getOrElse("none"))
println(parseKm("7").flatMap(km => if km > 5 then Left("too long") else Right(km)))
Right(6.0)
Left(not a number: x)
Left(error: not a number: x)
Right(not a number: x)
ok 7.0
7.0
0.0
Right(1.0)
Some(7.0)
None
true
Left(too short)
true
true
true
List(7.0)
Right(long enough)
Left(too short)
not a number: q
Left(too long)
Source: Scala API: Either
Note the line println(parseKm("x").swap): after swap the failure becomes a Right, which is why it prints Right(...).
Use it sparingly; most of the time left.map is what you want.
Pattern matching on Either
An Either is an ordinary sealed type, so a match works and the compiler checks that every case is covered. When
the failure type is an enum, matching on Left(SomeCase(...)) extracts the details. A list of Either values can be split into the
failures and the successes in one pass with partitionMap(identity).
//> using scala 3.10.0
package trailhead
enum TrailError:
case Blank
case NotANumber(text: String)
def parseKm(text: String): Either[TrailError, Double] =
if text.isBlank then Left(TrailError.Blank)
else text.toDoubleOption.toRight(TrailError.NotANumber(text))
def report(text: String): String = parseKm(text) match
case Right(km) => s"$km km"
case Left(TrailError.NotANumber(t)) => s"not a number: $t"
case Left(TrailError.Blank) => "empty input"
@main def run(): Unit =
println(report("4.5"))
println(report("abc"))
println(report(" "))
val results = List("1", "x", "3", "").map(parseKm)
val (errors, oks) = results.partitionMap(identity)
println(errors)
println(oks)
4.5 km
not a number: abc
empty input
List(NotANumber(x), Blank)
List(1.0, 3.0)
If you drop the Left(TrailError.Blank) case, the compiler warns that the match may not be exhaustive, which is the main
reason to use an enum for the error type.
Mixing different error types in a for comprehension
Each step in a for over Either may declare a different failure type. The compiler widens the left type to a common
supertype, which may be an unhelpful type such as a union or Object, and the result is hard to use:
//> using scala 3.10.0
package trailhead
def a(s: String): Either[String, Int] = s.toIntOption.toRight(s"not an int: $s")
def b(i: Int): Either[Exception, Int] = if i > 0 then Right(i) else Left(IllegalArgumentException("negative"))
@main def run(): Unit =
val r = for
x <- a("3")
y <- b(x)
yield x + y
val n: Int = r
-- [E007] Type Mismatch Error: ./ei_infer.scala:12:15
12 | val n: Int = r
| ^
| Found: (r : Either[String | Exception, Int])
| Required: Int
|
| longer explanation available when compiling with `-explain`
1 error found
Compilation failed
Source: Scala API: Either
The inferred type is the union String | Exception. That is correct but rarely what you want in a signature. Either map
the errors into one type of your own (left.map(TrailError.fromException)), or declare the union explicitly so that the
type is part of the API:
//> using scala 3.10.0
package trailhead
def a(s: String): Either[String, Int] = s.toIntOption.toRight(s"not an int: $s")
def b(i: Int): Either[Exception, Int] = if i > 0 then Right(i) else Left(IllegalArgumentException("negative"))
def total(s: String): Either[String | Exception, Int] =
for
x <- a(s)
y <- b(x)
yield x + y
@main def run(): Unit =
println(total("3"))
println(total("-3"))
println(total("q"))
Right(6)
Left(java.lang.IllegalArgumentException: negative)
Left(not an int: q)
Source: Scala API: Either
An error enum that the whole module shares is almost always the better design: callers match one type, and adding a failure
is a visible change.
Patterns in for generators
A tuple pattern on the left of <- is an irrefutable pattern (it cannot fail), so it works with Either on Scala 3.9 and
3.10:
//> using scala 3.10.0
package trailhead
def a(s: String): Either[String, Int] = s.toIntOption.toRight(s"not an int: $s")
@main def run(): Unit =
val r = for
(x, y) <- Right((1, 2))
z <- a("3")
yield x + y + z
println(r)
Right(6)
A refutable pattern, such as Some(h), can fail to match, which needs withFilter. Either has no withFilter (there is
no sensible Left to produce), so the case form is an error. Use a second generator that converts the option into an
Either:
//> using scala 3.10.0
package trailhead
def hutOf(s: String): Either[String, Option[String]] = Right(if s == "Ridge" then Some("Alto") else None)
@main def run(): Unit =
val r = for
case Some(h) <- hutOf("Ridge")
yield h
println(r)
-- [E008] Not Found Error: ./ei_refutable.scala:8:20
8 | case Some(h) <- hutOf("Ridge")
| ^^^^^^^^^^^^^^
| value withFilter is not a member of Either[String, Option[String]]
1 error found
Compilation failed
//> using scala 3.10.0
package trailhead
def hutOf(s: String): Either[String, Option[String]] = Right(if s == "Ridge" then Some("Alto") else None)
@main def run(): Unit =
val r = for
hut <- hutOf("Ridge")
name <- hut.toRight("no hut")
yield name
println(r)
Right(Alto)
The pattern rules, and the case form, are covered in For Comprehensions
and Extractors & Pattern Bindings.
Try: wrapping code that throws
Many APIs report failure by throwing. Try(expression) evaluates the expression and returns Success(value) if it
returns normally and Failure(throwable) if it throws a non-fatal exception. The result is an ordinary value that you can
return, store, pass around and transform, instead of a control-flow jump.
//> using scala 3.10.0
package trailhead
import scala.util.{Try, Success, Failure}
def parseKm(text: String): Try[Double] = Try(text.toDouble)
@main def run(): Unit =
println(parseKm("12.5"))
println(parseKm("abc"))
parseKm("abc") match
case Success(km) => println(km)
case Failure(e) => println(s"failed: ${e.getClass.getSimpleName}: ${e.getMessage}")
println(parseKm("5").map(_ + 1))
println(parseKm("x").map(_ + 1))
println(Try(1 / 0))
println(for a <- parseKm("1"); b <- parseKm("2") yield a + b)
println(for a <- parseKm("1"); b <- parseKm("x") yield a + b)
Success(12.5)
Failure(java.lang.NumberFormatException: For input string: "abc")
failed: NumberFormatException: For input string: "abc"
Success(6.0)
Failure(java.lang.NumberFormatException: For input string: "x")
Failure(java.lang.ArithmeticException: / by zero)
Success(3.0)
Failure(java.lang.NumberFormatException: For input string: "x")
Try is also right-biased and has the same shape of API as Either: map, flatMap, filter, foreach, fold,
getOrElse, orElse, toOption, toEither. It adds the operations that work on the exception:
-
recover(pf)turns a failure into a success when the partial function is defined for the exception. -
recoverWith(pf)does the same with a function that returns anotherTry. -
failedreturns aTry[Throwable]that succeeds with the exception when the original failed. -
transform(onSuccess, onFailure)handles both cases and returns a newTry.
//> using scala 3.10.0
package trailhead
import scala.util.{Try, Success, Failure}
def parseKm(text: String): Try[Double] = Try(text.toDouble)
@main def run(): Unit =
println(parseKm("x").recover { case _: NumberFormatException => 0.0 })
println(parseKm("x").recoverWith { case _: NumberFormatException => parseKm("1") })
println(parseKm("x").getOrElse(-1.0))
println(parseKm("x").orElse(parseKm("2")))
println(parseKm("5").filter(_ > 10))
println(parseKm("5").collect { case km if km > 1 => km * 2 })
println(parseKm("5").fold(_ => "bad", km => s"good $km"))
println(parseKm("x").failed.get.getClass.getName)
println(parseKm("5").transform(km => Success(km * 2), e => Failure(e)))
println(Try(Try(1)).flatten)
println(Try(Try(1)))
parseKm("3").foreach(km => println(s"got $km"))
println(parseKm("x").isFailure)
Success(0.0)
Success(1.0)
-1.0
Success(2.0)
Failure(java.util.NoSuchElementException: Predicate does not hold for 5.0)
Success(10.0)
good 5.0
java.lang.NumberFormatException
Success(10.0)
Success(1)
Success(Success(1))
got 3.0
true
Source: Scala API: Try
Note filter: a Success whose predicate fails becomes a Failure(NoSuchElementException). The exception comes with the
standard message "Predicate does not hold", which is rarely informative to an end user, so convert it to a domain error
if it can reach one.
What Try catches: NonFatal
Try does not catch every Throwable. It catches only the non-fatal ones, using the extractor
scala.util.control.NonFatal, and lets the fatal ones propagate. According to the API documentation, the fatal ones are
VirtualMachineError (which includes OutOfMemoryError and StackOverflowError), ThreadDeath, InterruptedException,
LinkageError and ControlThrowable. Everything else is non-fatal.
//> using scala 3.10.0
package trailhead
import scala.util.control.{ControlThrowable, NonFatal}
@main def run(): Unit =
println(NonFatal(new IllegalStateException("x")))
println(NonFatal(new NumberFormatException()))
println(NonFatal(new NotImplementedError()))
println(NonFatal(new InterruptedException()))
println(NonFatal(new LinkageError()))
println(NonFatal(new ControlThrowable {}))
println(NonFatal(new StackOverflowError()))
println(NonFatal(new OutOfMemoryError()))
try throw new IllegalStateException("closed")
catch case NonFatal(e) => println(s"recovered from ${e.getMessage}")
try throw new OutOfMemoryError("fake")
catch
case NonFatal(_) => println("non fatal")
case e: Throwable => println(s"left alone by NonFatal: ${e.getClass.getSimpleName}")
true
true
true
false
false
false
false
false
recovered from closed
left alone by NonFatal: OutOfMemoryError
Source: Scala API: NonFatal
The order of the output follows the order of the println calls: the first three are non-fatal, the last five (an
InterruptedException, a LinkageError, a ControlThrowable, a StackOverflowError and an OutOfMemoryError) are fatal.
NotImplementedError, the error thrown by ???, counts as non-fatal.
The practical effect is that Try will not swallow an out-of-memory condition:
//> using scala 3.10.0
package trailhead
import scala.util.Try
@main def run(): Unit =
println(Try(throw new IllegalStateException("closed")))
println(Try(throw new OutOfMemoryError("fake")))
Failure(java.lang.IllegalStateException: closed)
Exception in thread "main" java.lang.OutOfMemoryError: fake
at trailhead.tr_fatal$package$.run$$anonfun$2(tr_fatal.scala:8)
at scala.util.Try$.apply(Try.scala:256)
at trailhead.tr_fatal$package$.run(tr_fatal.scala:8)
at trailhead.run.main(tr_fatal.scala:6)
Source: Scala API: NonFatal
The first Try returned a Failure; the second one let the OutOfMemoryError escape and terminate the program. When you
write your own catch block, case NonFatal(e) => is the idiom for "catch what I can reasonably recover from". It is
described together with try/catch/finally in
Exceptions, boundary & Early Exit. A bare
catch case e: Throwable or catch case _ => is almost always a bug, because it also catches the fatal errors.
Resources with Using
scala.util.Using combines Try with resource management: it opens a resource that implements AutoCloseable, runs
the block, closes the resource whether the block succeeds or throws, and returns the result as a Try. Using.Manager
manages several resources and closes them in reverse order.
//> using scala 3.10.0
package trailhead
import scala.util.Using
class Hut(name: String) extends AutoCloseable:
println(s"open $name")
def beds: Int = if name == "bad" then throw new IllegalStateException("no beds") else 12
def close(): Unit = println(s"close $name")
@main def run(): Unit =
println(Using(Hut("alto"))(_.beds))
println(Using(Hut("bad"))(_.beds))
println(Using.Manager { use =>
val a = use(Hut("a"))
val b = use(Hut("b"))
a.beds + b.beds
})
open alto
close alto
Success(12)
open bad
close bad
Failure(java.lang.IllegalStateException: no beds)
open a
open b
close b
close a
Success(24)
Source: Scala API: Using
Java’s try-with-resources does the same job with a statement; Using makes it an expression whose result is a value.
See Java Exceptions for the Java form.
Converting between Option, Either and Try
The three types convert into each other, but each conversion drops or adds information, so choose the direction deliberately:
-
OptiontoEither:opt.toRight(error)adds a reason for the empty case.toLeftdoes the opposite. -
EithertoOption:either.toOptiondrops the failure. -
TrytoEither:try.toEitheralways has aThrowableon the left. Map it to a domain error withleft.map. -
EithertoTry:either.toTryneeds aLeftthat is aThrowable; the compiler enforces it with the implicit evidence<:<(error E172 below). -
TrytoOption:try.toOptiondrops the exception.
//> using scala 3.10.0
package trailhead
@main def run(): Unit =
val present: Option[Int] = Some(4)
val absent: Option[Int] = None
println(present.toRight("missing"))
println(absent.toRight("missing"))
println(Right(3).toOption)
println(Left("e").toOption)
val e: Either[Throwable, Int] = Left(new IllegalStateException("bad"))
println(e.toTry)
println(Right(5).toTry)
Right(4)
Left(missing)
Some(3)
None
Failure(java.lang.IllegalStateException: bad)
Success(5)
Source: Scala API: Either and Scala API: Try
//> using scala 3.10.0
package trailhead
import scala.util.Try
@main def run(): Unit =
println(Try("12".toInt).toEither)
println(Try("x".toInt).toEither.left.map(_.getClass.getSimpleName))
println(Try("12".toInt).toOption)
println(Try("x".toInt).toOption)
println(Try(Option(null: String)))
println(Try("x".toInt).toEither.left.map(_.getMessage))
Right(12)
Left(NumberFormatException)
Some(12)
None
Success(None)
Left(For input string: "x")
Source: Scala API: Either and Scala API: Try
toTry demands a Throwable on the left:
//> using scala 3.10.0
package trailhead
@main def run(): Unit =
println(Left("e").toTry)
-- [E172] Type Error: ./ei_totry_err.scala:5:25
5 | println(Left("e").toTry)
| ^
| Cannot prove that String <:< Throwable.
1 error found
Compilation failed
Source: Scala API: Either and Scala API: Try
An important detail is the shape of a Try around an optional value: Try(Option(null: String)) is Success(None), a
Try that succeeded with an empty option. It is two layers of "might not be there", and usually needs a flatMap or
toOption to flatten into one.
Accumulating errors
Either and Try are fail-fast: the first Left or Failure ends the computation, so a form with three invalid
fields reports only the first. When you want every problem reported, you need a type that combines the errors.
The standard library does not have one, but the shape is easy to write by hand when there are a few independent checks: run
them all, then collect the lefts.
//> using scala 3.10.0
package trailhead
case class Booking(name: String, hikers: Int, km: Double)
def name(s: String): Either[String, String] =
if s.isBlank then Left("name is blank") else Right(s)
def hikers(s: String): Either[String, Int] =
s.toIntOption.filter(_ > 0).toRight(s"hikers must be a positive int: $s")
def km(s: String): Either[String, Double] =
s.toDoubleOption.toRight(s"km must be a number: $s")
def booking(n: String, h: String, k: String): Either[List[String], Booking] =
(name(n), hikers(h), km(k)) match
case (Right(a), Right(b), Right(c)) => Right(Booking(a, b, c))
case (x, y, z) => Left(List(x, y, z).collect { case Left(e) => e })
def traverse[A, E, B](xs: List[A])(f: A => Either[E, B]): Either[E, List[B]] =
xs.foldRight(Right(Nil): Either[E, List[B]]): (a, acc) =>
for
b <- f(a)
bs <- acc
yield b :: bs
@main def run(): Unit =
println(booking("Ana", "2", "12.5"))
println(booking("", "0", "far"))
println(traverse(List("1", "2", "3"))(a => a.toIntOption.toRight(s"bad $a")))
println(traverse(List("1", "x", "y"))(a => a.toIntOption.toRight(s"bad $a")))
Right(Booking(Ana,2,12.5))
Left(List(name is blank, hikers must be a positive int: 0, km must be a number: far))
Right(List(1, 2, 3))
Left(bad x)
The first function validates three independent fields and returns a list of every failure; the second one,
traverse, applies one fallible function to a list and stops at the first failure, which is the fail-fast behaviour of the
for comprehension. For a handful of fields hand-written code like this is enough.
The library most often used to do this at scale is Cats, and its Validated type. This is a third-party library, not part of
Scala. It accumulates errors in a NonEmptyList and combines independent validations with mapN. In Scala CLI the
dependency is the //> using dep org.typelevel::cats-core:2.13.0 directive at the top of the example below; in sbt:
libraryDependencies += "org.typelevel" %% "cats-core" % "2.13.0"
Source: Cats documentation: Validated
//> using scala 3.10.0
//> using dep org.typelevel::cats-core:2.13.0
package trailhead
import cats.data.ValidatedNel
import cats.syntax.all.*
case class Booking(name: String, hikers: Int, km: Double)
def name(s: String): ValidatedNel[String, String] =
if s.isBlank then "name is blank".invalidNel else s.validNel
def hikers(s: String): ValidatedNel[String, Int] =
s.toIntOption.filter(_ > 0).toValidNel(s"hikers must be a positive int: $s")
def km(s: String): ValidatedNel[String, Double] =
s.toDoubleOption.toValidNel(s"km must be a number: $s")
@main def run(): Unit =
println((name("Ana"), hikers("2"), km("12.5")).mapN(Booking.apply))
println((name(""), hikers("0"), km("far")).mapN(Booking.apply))
Valid(Booking(Ana,2,12.5))
Invalid(NonEmptyList(name is blank, hikers must be a positive int: 0, km must be a number: far))
The program was fetched from Maven Central and run for real. Validated is not a monad (it has no flatMap, because a
flatMap would stop at the first error), which is exactly what lets it accumulate. Other libraries (ZIO Prelude and Scalaz both
have a Validation type) offer the same idea; the standard library does not. See
Ecosystem for the libraries in this area.
Early exit instead of nested Eithers
A for comprehension is one way to chain fallible steps. When the logic is a loop or an imperative sequence, the
scala.util.boundary mechanism returns an Either from the middle of the code without nesting. It is covered in
Exceptions, boundary & Early Exit; here is the
shape, applied to an Either:
//> using scala 3.10.0
package trailhead
import scala.util.{Try, Either}
import scala.util.boundary, boundary.break
def firstBad(xs: List[String]): Either[String, List[Int]] =
boundary:
Right(xs.map(x => x.toIntOption.getOrElse(break(Left(s"bad: $x")))))
@main def run(): Unit =
println(firstBad(List("1", "2")))
println(firstBad(List("1", "x", "y")))
Right(List(1, 2))
Left(bad: x)
Source: Scala API: boundary
Choosing a strategy
The decision depends on whether the failure is expected, whether the caller needs to know why, and whether you are wrapping code that throws.
-
Exceptions for programming errors and truly exceptional conditions that callers are not expected to handle: a violated precondition, a bug, an out-of-memory condition. They are fast on the success path and do not clutter the signature.
-
Optionwhen absence is the only failure and the reason does not matter: a lookup that finds nothing. -
Eitherfor expected, domain-level failures that the caller must handle. Choose anenumas the left type. -
Tryto wrap code that throws, typically Java, at the boundary. Convert it toEitherwith a domain error as soon as you can, so thatThrowabledoes not leak into the rest of your model. -
Accumulating validation when a user is waiting for a complete list of problems.
A rule of thumb for APIs: public methods return Option or Either with an error type that they define, and Try stays
inside the implementation. The reason is that the left type of Try is always Throwable, so it says nothing about what
can go wrong. For asynchronous code the equivalent is Future, which carries a Try inside (see
Futures & Promises).
Comparison with Java and Kotlin
-
Java reports failure with checked and unchecked exceptions, plus
Optionalfor absence. Checked exceptions put the failure in the method signature;Either[MyError, A]does the same without exceptions, and so often replaces them. Scala has no checked exceptions in the language (an experimentalCanThrowmechanism exists, see the exceptions page). See Java Exceptions. -
Kotlin has
kotlin.Result(a wrapper for success or an exception, close toTry) and idiomatic code uses exceptions and nullable types; there is no standardEither. See Kotlin Null Safety for the nullable side. -
TryandEitherare values, so they work with higher-order functions and collections, which is easier thantry/catchinside amaplambda; in Java a lambda cannot throw a checked exception without wrapping it.
Pitfalls
-
Leftis only a convention. Nothing stops you from using the wrong side for failure, and APIs that returnEitherwith the failure on the right, or with the same type on both sides, are a common source of bugs. Write theEither[Error, Value]type explicitly in the signature. -
Inferred types of
Left(...)andRight(...).Right(1)has typeRight[Nothing, Int], and a plainvalkeeps that narrow type. Ascribe the type when you store anEither(val r: Either[String, Int] = Right(1)), or write the return type of the function, which is a good habit anyway. -
Eitherbefore 2.12 was unbiased. Code and books written for Scala 2.11 useeither.right.map(...)andeither.left.map(...). Theleftprojection is still the way to work on the failure, but.rightis deprecated since 2.13.0 and warns on Scala 3:
//> using scala 3.10.0
//> using option -deprecation
package trailhead
@main def run(): Unit =
val r: Either[String, Int] = Right(2)
println(r.right.map(_ + 1))
-- Deprecation Warning: ./ei_right_proj.scala:7:12
7 | println(r.right.map(_ + 1))
| ^^^^^^^
|method right in class Either is deprecated since 2.13.0: Either is now right-biased, use methods directly on Either
1 warning found
Right(3)
Source: Scala API: Either
On current versions call map directly.
-
Tryswallows the stack position of the error that you care about.Failure(e)keeps the stack trace of the throwable, butTry(...).toOptionand.getOrElsediscard it. Log the exception before converting. -
Catching
Throwable. Nevercatch case _ =>; useNonFatal. -
Side effects inside a
Try.Tryevaluates its argument immediately and only once; it is not lazy. For deferred evaluation useFutureor an effect library (see Ecosystem). -
Try[Option[A]]andEither[E, Option[A]]. They are valid, but consider whetherOptionshould move to the error side: an absent value is often aLeft(NotFound).
Where the books are outdated
-
Scala 2.11-era material uses
.rightand.leftprojections to work with anEither. Since 2.12Eitheris right-biased andmap/flatMap/forwork directly. In Scala 3 you writeforandmap. -
Books that show
Trywithmatch { case Success(x) => ... case Failure(e) => ... }are right, but modern code prefersmap,foldandrecoverwhere it can. -
Books written before
scala.util.Using(2.13) close resources with a hand-writtentry/finallyor the Loan pattern. -
Material using
scala.util.control.Exception.catchingandallCatchworks, but is superseded in practice byTryandEither. -
The Scala 3 Book chapter on functional error handling states the basics but not the
NonFatalrule, error accumulation or the conversions, which this page covers.
Scala 3.3 LTS versus 3.9 and 3.10
All of the examples on this page gave the same output on 3.3.8, 3.9.0 and 3.10.0, except for one difference and the usual changes in the line numbers of a stack trace:
-
A tuple pattern as a generator over an
Either,(x, y) <- Right((1, 2)), fails on 3.3.8 with "value withFilter is not a member of Right[Nothing, (Int, Int)]". It compiles on 3.9.0 and 3.10.0, because irrefutable patterns no longer requirewithFilter. On the 3.3 LTS bind the value to a name and destructure it in a value definition inside the comprehension:
//> using scala 3.3.8
package trailhead
def a(s: String): Either[String, Int] = s.toIntOption.toRight(s"not an int: $s")
@main def run(): Unit =
val r = for
pair <- Right((1, 2))
(x, y) = pair
z <- a("3")
yield x + y + z
println(r)
Right(6)