Scala Error Handling with Option, Either, and Try
Edited by Dev Kilaru, Senior Data Analyst. How we make our content
Introduction
In many languages, a missing value is null and a failure is an exception, and neither shows up in a function's signature, so callers forget to handle them. Scala makes these cases part of the return type. A function that might not return a value returns an Option, one that can fail with a reason returns an Either, and code that might throw can be wrapped in a Try. The compiler then reminds callers to deal with the possibility.
Prerequisites
- Pattern matching from the previous lesson
- Collections and map, flatMap from earlier lessons
Step-by-Step Guide
Step 1: Represent a missing value with Option
An Option[A] is either Some containing a value, or None. A function that looks something up returns an Option so the possibility of nothing is visible in the type. The map method transforms the value if present and does nothing for None, so you can chain steps without checking for null at every stage. The getOrElse method supplies a default, and fold lets you handle both cases in a single expression. Prefer these over calling get, which throws if the Option is empty.
val users = Map(1 -> "Asha", 2 -> "Ben")
def findUser(id: Int): Option[String] = users.get(id)
println(findUser(1)) // Some(Asha)
println(findUser(9)) // None
println(findUser(1).map(_.toUpperCase)) // Some(ASHA)
println(findUser(9).getOrElse("guest")) // guest
println(findUser(2).fold("none")(n => s"hi $n")) // hi BenStep 2: Parse input safely
Methods such as toIntOption, toDoubleOption, and headOption return an Option instead of throwing when the input is bad. This is the right way to handle text from a user or a file, where bad values are normal, not exceptional. Combining it with flatMap lets you chain steps that may each fail, stopping at the first None. Collections have a flatMap that treats Option as a collection of zero or one element, which is a tidy way to drop the failures from a list.
"42".toIntOption // Some(42)
"forty-two".toIntOption // None
val inputs = List("10", "x", "30", "")
inputs.flatMap(_.toIntOption) // List(10, 30)
List.empty[Int].headOption // NoneStep 3: Return a reason for failure with Either
When the caller needs to know why something failed, use Either[E, A], where by convention Left carries the error and Right carries the success value. Either is right-biased, so map and flatMap work on the Right value and pass a Left through unchanged. This makes validation code compose well. Using a sealed trait or enum as the error type lets callers match on the specific reasons rather than parsing messages.
enum AgeError:
case NotANumber(text: String)
case OutOfRange(value: Int)
def parseAge(text: String): Either[AgeError, Int] =
text.toIntOption match
case None => Left(AgeError.NotANumber(text))
case Some(n) if n < 0 || n > 120 => Left(AgeError.OutOfRange(n))
case Some(n) => Right(n)
println(parseAge("34")) // Right(34)
println(parseAge("abc")) // Left(NotANumber(abc))
println(parseAge("200")) // Left(OutOfRange(200))Step 4: Wrap code that throws with Try
Many Java libraries signal problems by throwing exceptions. The Try type captures an exception thrown by a block of code and turns it into a value: Success holding the result, or Failure holding the exception. You can then use map, recover, and pattern matching instead of try and catch blocks scattered through the code. Use Try at the boundary where you call code that throws, and convert to Either or Option as you move into your own logic. Catch only what you can handle, since Try does not capture fatal errors such as running out of memory.
import scala.util.{Try, Success, Failure}
def safeDivide(a: Int, b: Int): Try[Int] = Try(a / b)
safeDivide(10, 2) // Success(5)
safeDivide(10, 0) // Failure(java.lang.ArithmeticException: / by zero)
safeDivide(10, 0) match
case Success(v) => println(s"result $v")
case Failure(e) => println(s"failed: ${e.getMessage}")Step 5: Chain steps with a for comprehension
A for comprehension works with Option, Either, and Try, because they all define map and flatMap. Each line extracts the value from a step, and if any step produces None or Left or Failure, the whole expression stops there and returns it, with no nested checks. The code reads like a straight-line sequence of steps while still handling every failure path. This is the main reason for using these types instead of exceptions: errors flow through the data, and the happy path stays easy to read.
def parsePositive(text: String): Either[String, Int] =
text.toIntOption.toRight(s"'$text' is not a number")
.filterOrElse(_ > 0, s"'$text' is not positive")
def total(a: String, b: String): Either[String, Int] =
for
x <- parsePositive(a)
y <- parsePositive(b)
yield x + y
println(total("5", "7")) // Right(12)
println(total("5", "-1")) // Left('-1' is not positive)
println(total("zz", "7")) // Left('zz' is not a number)Step 6: Interoperate with Java and decide when to throw
Java libraries may return null, and you can safely convert such a value with Option.apply, so that a null becomes None. Scala does not forbid exceptions, and it is reasonable to throw for programmer errors and truly unexpected situations, such as failing a require check on arguments. The rule of thumb is to use Option, Either, and Try for failures that callers should anticipate and handle, and exceptions for conditions that indicate a bug or an unrecoverable problem.
val fromJava: String = System.getenv("MAYBE_UNSET") // may be null
val safe = Option(fromJava) // None if null
println(safe.getOrElse("not set"))
def withdraw(balance: Double, amount: Double): Double =
require(amount > 0, "amount must be positive") // a bug if violated
balance - amountFAQs
- When should I use Option, Either, or Try?
- Use Option when absence is the only thing that can go wrong. Use Either when the caller needs to know why it failed, and especially for validation. Use Try to wrap calls that throw exceptions, mainly Java libraries and I/O, and convert the result into your own types.
- Why avoid calling .get on an Option?
- Calling get on None throws an exception, which brings back exactly the kind of runtime error that Option is meant to prevent. Use getOrElse, fold, map, or pattern matching so every case is handled.
- Is it wrong to use exceptions in Scala?
- No. They are appropriate for unexpected errors and programmer mistakes, and Java libraries throw them constantly. The point is to keep expected failures in the type system, where the compiler helps callers handle them.
Summary
You have seen how Option, Either, and Try put missing values and failures into types, how to transform and combine them with map, flatMap, and for comprehensions, and how to convert from null and exceptions at the boundary with Java. Code written this way is easier to read and test because the failure paths are explicit. The final lesson turns everything into a real project, built and tested with sbt.
Related tutorials
- Schedule and Debug Shell Scripts: cron, bash -x, and Portability
- Shell Scripting Project: A Backup Script with Rotation and a Log Report
- Writing Safe Bash Scripts: set -euo pipefail, getopts, and trap
- Shell Pipes, Redirection, grep, sed, and awk for Text Processing
- Bash Loops, Arrays, and Functions Explained
Comments
Log in or create a free account to join the discussion.