Scala Error Handling
Option, Try, or Either. The compiler then forces you to deal with the failure case before you can use the result.
You already met try/catch/finally in Control Flow. This page covers the functional alternative that most Scala code reaches for first.
Option
Option[A] represents a value that may or may not be present. It has exactly two subtypes: Some(value) when a value exists, and None when it does not. This replaces null everywhere in idiomatic Scala — a method that might not find a result returns Option[A] instead of a nullable A.
Example:
//a lookup that may fail returns Option instead of null
val ages = Map("Ana" -> 30, "Bob" -> 25)
def ageOf(name: String): Option[Int] = ages.get(name)
val a = ageOf("Ana") // Some(30)
val c = ageOf("Cris") // None
// safe defaults instead of null checks
println(a.getOrElse(0)) // 30
println(c.getOrElse(0)) // 0
// transform the value only if it is present
val nextYear = a.map(_ + 1) // Some(31)
// pattern matching reads like plain language
ageOf("Bob") match {
case Some(age) => println(s"Bob is $age")
case None => println("Bob is not registered")
}
.get on an Option in production code — it throws if the value is None, which defeats the purpose. Prefer getOrElse, pattern matching, map/flatMap, or a for-comprehension.Try
Try[A] represents a computation that might throw an exception. It has two subtypes: Success(value) and Failure(exception). Wrap any risky call — parsing, I/O, network — in Try { ... } and the exception is captured as a value instead of unwinding the stack.
Example:
import scala.util.{Try, Success, Failure}
def parseInt(s: String): Try[Int] = Try(s.toInt)
parseInt("42") match {
case Success(n) => println(s"Parsed: $n")
case Failure(ex) => println(s"Could not parse: ${ex.getMessage}")
}
// chain transformations without nested try/catch
val doubled = parseInt("21").map(_ * 2) // Success(42)
val failed = parseInt("oops").recover { case _: NumberFormatException => -1 } // Success(-1)
// convert to Option when you only care whether it worked
val maybe: Option[Int] = parseInt("42").toOption // Some(42)
Either
Either[L, R] represents one of two possible outcomes: Left(value) or Right(value). By convention Left carries the failure (often an error message or error type) and Right carries the success — think "right is right". Unlike Try, the error does not have to be a Throwable, so it is the better fit for domain validation.
Example:
def divide(a: Int, b: Int): Either[String, Int] =
if (b == 0) Left("division by zero") else Right(a / b)
divide(10, 2) match {
case Right(result) => println(s"Result: $result")
case Left(error) => println(s"Error: $error")
}
// Either is right-biased: map/flatMap operate on Right, Left passes through untouched
val chained = divide(10, 2).map(_ + 1).flatMap(n => divide(n, 0))
println(chained) // Left(division by zero)
Composing With For-Comprehensions
Option, Try, and Either all support map, flatMap, and filter, which means every one of them works inside a for-comprehension. This is the idiomatic way to chain several fallible steps without a pyramid of nested pattern matches.
Example:
def parseInt(s: String): Option[Int] = s.toIntOption
val sumOfTwo: Option[Int] = for {
a <- parseInt("10")
b <- parseInt("32")
} yield a + b
println(sumOfTwo) // Some(42)
// the moment one step fails, the whole comprehension short-circuits
val sumWithBadInput: Option[Int] = for {
a <- parseInt("10")
b <- parseInt("not a number")
} yield a + b
println(sumWithBadInput) // None
When To Still Use Exceptions
Container types are not a replacement for every exception. Use plain try/catch (see Exceptions) when:
- You must guarantee cleanup with
finally, such as closing a file or a connection; - You are calling into a Java library that already throws checked or unchecked exceptions;
- The failure is truly exceptional and unrecoverable — a programming bug, not an expected outcome.
Option, Try, or Either. If it is a bug or a truly unrecoverable condition, let an exception propagate.