100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Error Handling with Try and Either

Scala models recoverable failure as ordinary values using Try and Either, letting you compose error-prone code with map, flatMap, and for-comprehensions instead of throwing and catching exceptions.

Advanced ScalaIntermediate9 min readJul 10, 2026
Analogies

Why Model Errors as Values

Throwing exceptions has two well-known downsides in a statically typed language like Scala: a method's signature gives no indication that it might fail (def parse(s: String): Int looks total but can throw NumberFormatException), and an uncaught exception unwinds the call stack, forcing every intermediate caller to either handle it or implicitly propagate it. Try[T] and Either[L, R] fix this by making failure part of the return type - a caller who ignores the possibility of failure gets a compile-time visible problem, an unused Try or an unhandled Either pattern, not a runtime surprise, and because both types support map and flatMap, you compose error-prone operations the same way you compose any other value, with for-comprehensions short-circuiting cleanly on the first failure.

🏏

Cricket analogy: Like a scorecard that explicitly records 'retired hurt' as a distinct outcome type rather than just silently vanishing a batter from the lineup - Try and Either make failure a visible, recorded outcome in the type, not an invisible interruption like an uncaught exception.

Working with Try

Try(riskyOperation()) runs its argument eagerly and catches any non-fatal exception, wrapping the result as Success(value) or Failure(throwable) - critically, it only catches exceptions matched by scala.util.control.NonFatal, which deliberately excludes OutOfMemoryError, StackOverflowError, and similar JVM-level errors that indicate the program is in an unrecoverable state and shouldn't be swallowed. Like Option and Future, Try supports map, flatMap, recover (supply a fallback Success for a matched exception), and getOrElse (unwrap to a plain value with a default), making it the natural wrapper for interoperating with legacy or Java APIs that signal failure via thrown exceptions, without spreading try/catch blocks throughout your business logic.

🏏

Cricket analogy: Like a run-out review that catches genuinely close, reviewable decisions but explicitly doesn't apply to a bails-falling-off-in-wind non-event - Try's NonFatal filter similarly catches recoverable problems while deliberately letting truly catastrophic JVM failures pass through unhandled.

scala
import scala.util.{Try, Success, Failure}

def parseAge(input: String): Try[Int] = Try(input.trim.toInt)

parseAge("27") match {
  case Success(age)  => println(s"Parsed age: $age")
  case Failure(error) => println(s"Could not parse age: ${error.getMessage}")
}

// Composable with map/flatMap and a fallback via recover
val safeAge: Try[Int] = parseAge("not-a-number").recover { case _: NumberFormatException => 0 }
println(safeAge.getOrElse(-1))   // 0

Working with Either

Either[L, R] is a general sum type representing one of two possibilities, and by convention Right holds the success case, a mnemonic for 'right' meaning both correct and the right-hand side, while Left holds the error case; since Scala 2.12, Either is right-biased, meaning map and flatMap operate on the Right value directly without needing .right.map as in older Scala. Either's key advantage over Try is that you choose the error type yourself - instead of being locked into Throwable, you can define a sealed trait ValidationError with specific cases like EmptyField or InvalidFormat, giving callers exhaustive pattern matching and much more precise, domain-specific error information than a generic exception message ever could.

🏏

Cricket analogy: Like a review system that returns a specific structured verdict, Right(Out: caught behind) or Left(NotOut: bat-pad gap confirmed), rather than a generic 'decision changed' message, giving the broadcast a precise, typed reason instead of a vague Throwable-style note.

Try locks you into Throwable as the failure type, which loses precision - catching a Failure tells you that something threw, but pattern matching on arbitrary exception classes is brittle and un-exhaustive. Either lets you define your own closed, exhaustive error ADT, a sealed trait of specific case objects and classes, so the compiler can warn you if a match isn't exhaustive - prefer Either with a custom error type for domain validation logic, and reserve Try specifically for wrapping calls into exception-throwing Java or legacy APIs you don't control.

Combining Try, Either, and Validation

You convert between the two with .toEither (on Try, producing Either[Throwable, T]) and .toTry (on Either[Throwable, T] specifically, since toTry needs an implicit way to turn L into a Throwable), which is handy when a legacy API gives you a Try but the rest of your domain logic is built around a custom Either error type. One real limitation to know: a for-comprehension over multiple Eithers or Trys short-circuits on the first failure and discards the rest - if you validate a form with five fields and three are invalid, a plain Either-based for-comprehension only reports the first one, which is why accumulating-error validation, reporting all three at once, typically requires a dedicated applicative-validation type like Cats' Validated, not raw Either.

🏏

Cricket analogy: Like a DRS system that stops reviewing further angles the instant it confirms 'out' on the first camera check - it short-circuits rather than continuing to check every other possible dismissal type, the way a for-comprehension stops at the first Left.

Try's automatic catching is scoped by scala.util.control.NonFatal, which explicitly excludes VirtualMachineError, including OutOfMemoryError and StackOverflowError, InterruptedException, LinkageError, and ControlThrowable - these propagate uncaught even through a Try block, because catching them and continuing as if nothing happened is unsafe; a StackOverflowError, for instance, usually means the JVM's call stack is in an inconsistent state that shouldn't be papered over with a Success/Failure wrapper.

scala
import scala.util.Try

sealed trait ValidationError
case object EmptyField extends ValidationError
case class InvalidFormat(field: String) extends ValidationError

def parseAge(input: String): Try[Int] = Try(input.trim.toInt)

def validateAge(input: String): Either[ValidationError, Int] =
  if (input.trim.isEmpty) Left(EmptyField)
  else parseAge(input).toEither.left.map(_ => InvalidFormat("age"))

val result: Either[ValidationError, Int] = for {
  age <- validateAge("27")
} yield age

result match {
  case Right(age)             => println(s"Valid age: $age")
  case Left(EmptyField)       => println("Age is required")
  case Left(InvalidFormat(f)) => println(s"Invalid format for $f")
}
  • Try[T] and Either[L, R] make failure part of a function's return type, so callers can't silently ignore the possibility of failure the way they can with a thrown exception.
  • Try(...) catches only NonFatal exceptions, deliberately excluding JVM-fatal errors like OutOfMemoryError and StackOverflowError.
  • Either is right-biased since Scala 2.12, so map and flatMap operate directly on the Right value without .right.map.
  • Either lets you define your own domain-specific, exhaustively-matchable error type instead of being locked into Throwable like Try.
  • .toEither and .toTry convert between the two, useful when bridging a legacy exception-throwing API into a domain-driven Either pipeline.
  • A for-comprehension over multiple Eithers or Trys short-circuits on the first failure, discarding information about subsequent failures.
  • Accumulating multiple validation errors at once typically requires an applicative type like Cats' Validated, not raw Either.

Practice what you learned

Was this page helpful?

Topics covered

#Programming#ScalaStudyNotes#ErrorHandlingWithTryAndEither#Error#Handling#Try#Either#ErrorHandling#StudyNotes#SkillVeris