Technology  /  Scala

๐Ÿ”ด Scala 21 guides ยท updated 2026

The JVM's hybrid OOP/functional language โ€” immutability, pattern matching, and the type system that powers Spark and Akka, from first principles to production.

Option, Either and Try in Scala: Handling Absence and Failure Without Null

Tony Hoare, who invented the null reference in 1965, later called it his โ€œbillion-dollar mistakeโ€ โ€” a null pointer can hide behind any reference type in most languages, and the compiler never forces you to check for it, so it surfaces as a runtime crash at the worst possible moment. Scalaโ€™s answer is to make โ€œthis might not have a valueโ€ and โ€œthis might failโ€ explicit in the type system โ€” Option, Either, and Try โ€” so the compiler forces you to handle the missing/failure case at compile time, not discover it at runtime.


Option โ€” Explicit Absence Instead of Null

def findUser(id: String): Option[String] = {
val users = Map("1" -> "Alice", "2" -> "Bob")
users.get(id)
}
findUser("1") // Some("Alice")
findUser("99") // None

Option[T] has exactly two possible values: Some(value) (a value is present) or None (nothing is present) โ€” and critically, findUserโ€™s return type tells the caller, in the type signature itself, that a user might not be found. This is fundamentally different from a Java method returning String that might secretly return null โ€” nothing in that Java signature warns you, and nothing forces you to check before using the result. Option[T] makes the possibility of absence part of the contract, checked by the compiler.

val result = findUser("99")
result.length // Compile error โ€” Option[String] has no .length method; you must unwrap it first

This is the actual safety mechanism: you cannot accidentally call a String method directly on an Option[String] โ€” the compiler forces you to explicitly handle both cases before getting to the underlying value, eliminating the entire โ€œforgot to null-checkโ€ class of bug structurally, not just by convention.

Safely extracting a value

val name1 = findUser("1").getOrElse("Unknown") // "Alice"
val name2 = findUser("99").getOrElse("Unknown") // "Unknown" โ€” fallback used
findUser("1").isDefined // true
findUser("99").isEmpty // true
findUser("1") match {
case Some(name) => println(s"Found: $name")
case None => println("Not found")
}

getOrElse is the most common way to unwrap an Option with a sensible default; pattern matching is the most explicit way when both branches need genuinely different handling, not just a fallback value.

Chaining operations without ever checking isDefined manually

def findUser(id: String): Option[String] = Map("1" -> "Alice").get(id)
def findEmail(name: String): Option[String] = Map("Alice" -> "alice@example.com").get(name)
val email = findUser("1").flatMap(findEmail)
// Some("alice@example.com") โ€” chains two Option-returning lookups without any manual null/isDefined checks
val email2 = findUser("1")
.map(_.toUpperCase)
.flatMap(findEmail)
.getOrElse("no-email@example.com")

This is exactly the flatMap-on-Option idiom previewed in the collection operations guide: Option behaves as a zero-or-one-element collection, so map/flatMap chain naturally โ€” if any step in the chain produces None, every subsequent step is automatically skipped, and the whole chain short-circuits to None without any explicit if checks at each step.

Some('Alice')

Some('alice@example.com')

None

findUser('1')

flatMap(findEmail)

Result: Some(email)

findUser('99')

flatMap short-circuits automatically

Result: None โ€” findEmail never even runs


Either โ€” Success or Failure, With a Reason

def parseAge(input: String): Either[String, Int] = {
input.toIntOption match {
case Some(age) if age >= 0 => Right(age)
case Some(_) => Left("Age cannot be negative")
case None => Left(s"'$input' is not a valid number")
}
}
parseAge("30") // Right(30)
parseAge("-5") // Left("Age cannot be negative")
parseAge("abc") // Left("'abc' is not a valid number")

Either[L, R] generalizes Option for the case where you need to explain why something failed, not just that it did โ€” by strong convention, Left carries the failure/error case and Right carries the success case (a mnemonic: โ€œrightโ€ also means โ€œcorrectโ€). This is the idiomatic Scala replacement for exceptions in ordinary business logic โ€” validation, parsing, anything with an expected, describable failure mode that the caller should handle as a normal code path, not an exceptional one.

// Chaining Either the same way as Option
def validateAge(age: Int): Either[String, Int] =
if (age <= 150) Right(age) else Left("Age seems implausible")
val result = parseAge("30").flatMap(validateAge)
// Right(30) โ€” both validations passed
val result2 = parseAge("-5").flatMap(validateAge)
// Left("Age cannot be negative") โ€” short-circuits at the first failure, validateAge never runs

Just like Option, Either short-circuits on the first Left in a chain of flatMap calls โ€” this is exactly why Either (specifically its flatMap) is the standard building block for validation pipelines with multiple sequential checks, each of which can fail with its own specific error message.

// Pattern matching to handle both branches explicitly
parseAge("abc") match {
case Right(age) => println(s"Valid age: $age")
case Left(error) => println(s"Error: $error")
}

Try โ€” Wrapping Code That Might Throw

import scala.util.{Try, Success, Failure}
def parseNumber(s: String): Try[Int] = Try(s.toInt)
parseNumber("42") // Success(42)
parseNumber("abc") // Failure(java.lang.NumberFormatException: For input string: "abc")
parseNumber("42") match {
case Success(n) => println(s"Parsed: $n")
case Failure(ex) => println(s"Failed: ${ex.getMessage}")
}

Try[T] wraps a computation that might throw an exception, capturing the result as either Success(value) or Failure(exception) โ€” the specific value of Try is bridging existing, exception-throwing Java/Scala APIs (like String.toInt, which throws NumberFormatException) into the same safe, chainable style as Option/Either, without you needing to wrap every call in a manual try/catch block.

val result = parseNumber("10")
.map(_ * 2)
.flatMap(n => if (n > 0) Success(n) else Failure(new IllegalArgumentException("Must be positive")))
.getOrElse(0)

Try supports the identical map/flatMap/getOrElse chaining vocabulary as Option and Either โ€” this consistency across all three types is deliberate, letting you learn one chaining pattern and apply it regardless of which of the three youโ€™re working with.

Converting between the three

val opt: Option[Int] = parseNumber("42").toOption // Try -> Option, discarding the exception detail
val either: Either[Throwable, Int] = parseNumber("42").toEither // Try -> Either, keeping the exception as Left
val tryFromOption: Try[Int] = Try(opt.get) // Option -> Try (risky โ€” .get throws on None)

Converting between the three is common in real code: Try.toOption when you donโ€™t care why something failed, only whether it succeeded; Try.toEither when you want to propagate the failure reason further as an Either for uniform handling alongside other validation logic.


When to Use Which

TypeRepresentsUse it when
Option[T]Present or absent, no reason neededLooking up a value that might not exist (map lookup, find in a list, optional config field)
Either[L, R]Success or failure, with an explanationValidation, business-rule checks, anything needing a specific, meaningful error message
Try[T]Success or a caught exceptionWrapping calls to exception-throwing APIs (parsing, I/O, interop with Java libraries)

A common, idiomatic real-world pipeline uses all three together: Try to safely call an exception-throwing library function, .toEither to convert the caught exception into your own domain-specific error type, then flatMap-chaining that Either with further Either-returning validation logic โ€” each type used exactly where its specific strength applies.


Common Patterns

Collecting only the present values from a list of Options

val inputs = List("1", "2", "abc", "4")
val parsed: List[Option[Int]] = inputs.map(_.toIntOption)
val valid: List[Int] = parsed.flatten
// List(1, 2, 4) โ€” .flatten on a List[Option[T]] drops every None and unwraps every Some
// Equivalent, more direct one-step version using flatMap (from the collection operations guide)
val validDirect = inputs.flatMap(_.toIntOption) // List(1, 2, 4)

Requiring every value to be present, or failing entirely

val inputs = List("1", "2", "3")
val allParsed: Option[List[Int]] = inputs.foldLeft(Option(List.empty[Int])) { (accOpt, s) =>
for {
acc <- accOpt
n <- s.toIntOption
} yield acc :+ n
}
// Some(List(1, 2, 3)) if every element parses; None if even one fails
// The standard-library shortcut for exactly this case
val sequenced: Option[List[Int]] = inputs.map(_.toIntOption).reduceOption((a, b) => for { x <- a; y <- b } yield x)

This โ€œall must succeed, or the whole thing failsโ€ pattern โ€” turning a List[Option[T]] into an Option[List[T]] โ€” comes up often enough in validation logic that libraries like Cats provide a direct sequence method for it; the manual foldLeft version above shows what that convenience is actually doing underneath.

Recovering from a failure with a default computation

import scala.util.{Try, Success, Failure}
def fetchFromCache(key: String): Try[String] = Failure(new RuntimeException("cache miss"))
def fetchFromDatabase(key: String): String = s"value-for-$key"
val result = fetchFromCache("user:1").recover {
case _: RuntimeException => fetchFromDatabase("user:1")
}
// Success("value-for-user:1") โ€” recover turns a Failure into a Success using a fallback

recover is Tryโ€™s equivalent of getOrElse for cases where the fallback itself might reasonably depend on which exception occurred, rather than always being the same fixed default value.

Why Not Just Use Exceptions Everywhere?

// Exception-based โ€” the possibility of failure is invisible in the type signature
def parseAgeUnsafe(input: String): Int = input.toInt // no hint this can throw!
// Callers have no compile-time signal to handle the failure case at all
val age = parseAgeUnsafe("not a number") // crashes at runtime, with zero warning from the type checker

The core problem with unchecked exceptions (which is what Javaโ€™s RuntimeException hierarchy and Scalaโ€™s default exception model both are) is that a functionโ€™s signature gives no indication it might throw โ€” parseAgeUnsafe(input: String): Int looks exactly like a function that always succeeds, and the only way to discover otherwise is documentation, tribal knowledge, or a crash in production. Option/Either/Try make the possibility of failure a visible, checked part of the type signature โ€” a caller literally cannot get an Int out of parseAge without going through code that acknowledges failure might have happened, whether via getOrElse, pattern matching, or further chaining.


Frequently Asked Questions

Should I ever use null in Scala? Essentially never in idiomatic code โ€” Option exists specifically to replace it, and Scalaโ€™s own standard library and ecosystem libraries consistently use Option rather than null for โ€œmight be absentโ€ values. null remains present (Scala runs on the JVM, and Java interop can hand you nulls) but should be converted to Option at the boundary (Option(javaNullableValue), which correctly produces None if the value is null) rather than propagated through your own code.

Whatโ€™s the actual difference between Option and Either if both can represent โ€œnothingโ€? Optionโ€™s None carries no information about why โ€” itโ€™s binary, present or absent. Eitherโ€™s Left carries an actual value (typically an error message or error type) explaining the failure, which is the entire reason to reach for Either over Option whenever the caller needs to know more than just โ€œit didnโ€™t work.โ€

Does wrapping everything in Try eliminate the need to ever write a try/catch block? Mostly, for the common case of โ€œcall a single throwing function and represent its result safelyโ€ โ€” but genuinely complex exception handling (different behavior per exception type, resource cleanup needing finally) may still be clearer as an explicit try/catch, with the result converted to Try/Either immediately afterward for the rest of the pipeline.

Is there a performance cost to using Option/Either/Try instead of null/exceptions? A small, generally negligible allocation cost for wrapping a value in Some/Right/Success โ€” in exchange for compiler-enforced handling of the failure case. For the overwhelming majority of application code, this trade-off strongly favors safety; only in extremely hot, allocation-sensitive loops would this cost be worth specifically avoiding.

Can I chain Option, Either, and Try together with a for comprehension instead of explicit flatMap calls? Yes โ€” this is exactly what the for-comprehensions guide covers, and itโ€™s the more common, more readable way to write a multi-step chain of any of these three types in real code, rather than a long chain of explicit .flatMap(...).flatMap(...) calls.


Whatโ€™s Next

Option, Either, and Try make absence and failure explicit, type-checked parts of your code rather than hidden runtime surprises. Next: Pattern Matching in Scala, covering match expressions, extractors, and sealed trait exhaustiveness checking in full depth โ€” building directly on the Some/None/Right/Left pattern matching already used throughout this guide.