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 type E (a Left) or a success of type A (a Right). 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

map(f), flatMap(f)

Transform or chain the Right; a Left is returned as it is.

left.map(f)

Transforms the Left; the left projection is the way to work with the failure.

swap

Exchanges the two sides.

fold(onLeft, onRight)

Collapses both cases to one result type.

getOrElse(default), orElse(alternative)

Fallback value, or fallback Either, for a Left.

filterOrElse(p, leftValue)

Turns a Right into a Left when the predicate fails. Either has no filter, because it would not know which Left to produce.

exists(p), forall(p), contains(x)

Tests on the Right value.

toOption, toSeq, toTry

Conversions (see below).

Either.cond(test, right, left)

Builds an Either from a condition.

//> 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)

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

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)

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

  • failed returns a Try[Throwable] that succeeds with the exception when the original failed.

  • transform(onSuccess, onFailure) handles both cases and returns a new Try.

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

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)

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)

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:

  • Option to Either: opt.toRight(error) adds a reason for the empty case. toLeft does the opposite.

  • Either to Option: either.toOption drops the failure.

  • Try to Either: try.toEither always has a Throwable on the left. Map it to a domain error with left.map.

  • Either to Try: either.toTry needs a Left that is a Throwable; the compiler enforces it with the implicit evidence <:< (error E172 below).

  • Try to Option: try.toOption drops 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)
//> 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")

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

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"
//> 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)

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.

flowchart TD Start["A function can fail"] --> Q1{"Is the failure a programming error or a violated precondition?"} Q1 -->|"yes"| EX["Throw an exception: require, assert, IllegalStateException"] Q1 -->|"no, it is expected"| Q2{"Does the caller need to know why?"} Q2 -->|"no, absence is enough"| OP["Option"] Q2 -->|"yes"| Q3{"Does the failure come from code that throws, such as Java APIs?"} Q3 -->|"yes"| TR["Try, or convert at the boundary with toEither"] Q3 -->|"no"| Q4{"Should every error be reported, not only the first?"} Q4 -->|"yes"| VA["Accumulate: a list of Left, or Validated from Cats"] Q4 -->|"no"| EI["Either with an error enum"]
  • 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.

  • Option when absence is the only failure and the reason does not matter: a lookup that finds nothing.

  • Either for expected, domain-level failures that the caller must handle. Choose an enum as the left type.

  • Try to wrap code that throws, typically Java, at the boundary. Convert it to Either with a domain error as soon as you can, so that Throwable does 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 Optional for 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 experimental CanThrow mechanism exists, see the exceptions page). See Java Exceptions.

  • Kotlin has kotlin.Result (a wrapper for success or an exception, close to Try) and idiomatic code uses exceptions and nullable types; there is no standard Either. See Kotlin Null Safety for the nullable side.

  • Try and Either are values, so they work with higher-order functions and collections, which is easier than try/catch inside a map lambda; in Java a lambda cannot throw a checked exception without wrapping it.

Pitfalls

  • Left is only a convention. Nothing stops you from using the wrong side for failure, and APIs that return Either with the failure on the right, or with the same type on both sides, are a common source of bugs. Write the Either[Error, Value] type explicitly in the signature.

  • Inferred types of Left(...) and Right(...). Right(1) has type Right[Nothing, Int], and a plain val keeps that narrow type. Ascribe the type when you store an Either (val r: Either[String, Int] = Right(1)), or write the return type of the function, which is a good habit anyway.

  • Either before 2.12 was unbiased. Code and books written for Scala 2.11 use either.right.map(...) and either.left.map(...). The left projection is still the way to work on the failure, but .right is 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)

On current versions call map directly.

  • Try swallows the stack position of the error that you care about. Failure(e) keeps the stack trace of the throwable, but Try(...).toOption and .getOrElse discard it. Log the exception before converting.

  • Catching Throwable. Never catch case _ =>; use NonFatal.

  • Side effects inside a Try. Try evaluates its argument immediately and only once; it is not lazy. For deferred evaluation use Future or an effect library (see Ecosystem).

  • Try[Option[A]] and Either[E, Option[A]]. They are valid, but consider whether Option should move to the error side: an absent value is often a Left(NotFound).

Where the books are outdated

  • Scala 2.11-era material uses .right and .left projections to work with an Either. Since 2.12 Either is right-biased and map/flatMap/for work directly. In Scala 3 you write for and map.

  • Books that show Try with match { case Success(x) => ... case Failure(e) => ... } are right, but modern code prefers map, fold and recover where it can.

  • Books written before scala.util.Using (2.13) close resources with a hand-written try/finally or the Loan pattern.

  • Material using scala.util.control.Exception.catching and allCatch works, but is superseded in practice by Try and Either.

  • The Scala 3 Book chapter on functional error handling states the basics but not the NonFatal rule, 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 require withFilter. 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)