Scala Error Handling

Scala favors values over exceptions. Instead of throwing and hoping someone up the call stack remembers to catch, idiomatic Scala wraps a missing value, a risky computation, or a choice between two outcomes in a small container type: 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")
}
Tip: Never call .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.
Rule of thumb: if the failure is an expected, everyday outcome of calling the function (not found, invalid input, network timeout), return Option, Try, or Either. If it is a bug or a truly unrecoverable condition, let an exception propagate.