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.
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)) // 0Working 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.
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
1. What is the main problem with communicating errors via thrown exceptions in Scala's type system?
2. Which exceptions does Try(...) deliberately NOT catch?
3. What does it mean that Either has been 'right-biased' since Scala 2.12?
4. What is the main advantage of Either over Try for domain validation logic?
5. What happens when a for-comprehension combines multiple Either values and the second one is a Left?
Was this page helpful?
You May Also Like
Futures and Concurrency
Scala's Future abstraction models asynchronous computations that run on an ExecutionContext, letting you compose non-blocking operations with map, flatMap, and for-comprehensions.
Implicits in Scala
Scala's implicit parameters, implicit conversions, and implicit classes let the compiler thread context and behavior through code automatically, powering type classes and DSLs.
Scala and Akka Actors
Akka's actor model gives Scala developers a way to build concurrent, distributed systems out of isolated, message-driven actors instead of shared mutable state and locks.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics