Free Handbook · Every example compiled & verified

Option, Either & Try

Model missing values with Option, failures with Either and exceptions with Try, chain them with map, flatMap and for, and build a validation pipeline.

0 / 142 lessons🔥 0 day streak
ShareXLinkedIn

Module 08 · what you'll be able to do

  • Replace null with Option and read it safely with map, flatMap, getOrElse and fold
  • Return Either from functions that can fail for expected reasons, and chain them in a for-comprehension
  • Wrap exception-throwing code in Try and recover from Failure
  • Convert between Option, Either and Try, and choose which one a function should return
  • Build a small validation pipeline that reports every bad record instead of crashing on the first
01

Option: Some, None and no more null

The null reference is often called the billion-dollar mistake: any reference might be null, the type does not say so, and forgetting to check crashes at runtime. Scala code uses Option[A] instead. An Option[Int] is either Some(42) — a value is present — or None. Because the type is Option[Int] and not Int, the compiler will not let you use it as a number until you have decided what to do about None.

scalaMain.scala
case class User(id: Int, name: String, email: Option[String])

val users = List(
  User(1, "Asha", Some("[email protected]")),
  User(2, "Ravi", None)
)

def findUser(id: Int): Option[User] = users.find(_.id == id)

@main def run(): Unit =
  println(findUser(1))
  println(findUser(9))
  println(findUser(2).map(_.name))
  println(findUser(2).flatMap(_.email))
  println(findUser(1).flatMap(_.email).getOrElse("no email"))
  println(findUser(9).isDefined)

  val fromJava: String = null
  println(Option(fromJava))
  println(Option("x"))
Outputcompiled & run with real Scala
Some(User(1,Asha,Some([email protected])))
None
Some(Ravi)
None
[email protected]
false
None
Some(x)

find already returns an Option. Wrapping a possibly-null value from a Java library with Option(...) turns null into None at the boundary, so the rest of your code never sees null. Some(null), by contrast, is a Some — always use Option(...) for Java values.

Your turn

Add a third user with an email and print the domain part of user 3's email (.map(_.split("@")(1))), with "unknown" as the fallback.

Error you will hit

Calling get on None

scala
@main def run(): Unit =
  val config = Map("host" -> "localhost")
  val port = config.get("port").get
  println(port)
Exception in thread "main" java.util.NoSuchElementException: None.get
	at scala.None$.get(Option.scala:700)
	at scala.None$.get(Option.scala:700)
	at Main$package$.run(Main.scala:3)
	at run.main(Main.scala:1)
Why the compiler said that

get on an Option means "I am sure this is a Some". When it is None, there is no value to return, so it throws — which is exactly the crash Option was meant to prevent. Calling .get gives up all the safety.

The fix

Say what should happen when the value is missing: getOrElse for a default, match or fold to handle both cases, or turn it into an Either with a helpful message.

scala
@main def run(): Unit =
  val config = Map("host" -> "localhost")
  val port = config.get("port").getOrElse("8080")
  println(port)
02

Working with Option: map, flatMap, getOrElse, fold

Think of an Option as a collection that holds zero or one element. That is why it has the same methods as List: map transforms the value if there is one, filter turns a Some that fails the test into None, and flatMap chains a step that itself returns an Option. At the end you usually leave the Option world with getOrElse or fold.

scalaMain.scala
def parsePort(s: String): Option[Int] = s.toIntOption.filter(p => p > 0 && p < 65536)

@main def run(): Unit =
  println(parsePort("8080"))
  println(parsePort("abc"))
  println(parsePort("99999"))

  println(parsePort("8080").map(_ + 1))
  println(parsePort("abc").map(_ + 1))
  println(parsePort("abc").getOrElse(80))
  println(parsePort("abc").orElse(parsePort("443")))

  val label = parsePort("22").fold("invalid port")(p => s"port $p")
  println(label)
  println(parsePort("22").exists(_ < 1024))
  println(parsePort("22").toList)
Outputcompiled & run with real Scala
Some(8080)
None
None
Some(8081)
None
80
Some(443)
port 22
true
List(22)

toIntOption is the safe version of toInt. fold(ifEmpty)(f) handles both cases in one expression: the first argument is used for None, the function for Some. orElse tries another Option when the first is empty.

When several steps each return an Option, a for-comprehension chains them (it is flatMap underneath, as Module 07 showed). The result is Some only if every step produced a value; the first None stops the chain.

scalaMain.scala
val env = Map("DB_HOST" -> "db.local", "DB_PORT" -> "5432", "BAD_PORT" -> "x")

def connString(hostKey: String, portKey: String): Option[String] =
  for
    host <- env.get(hostKey)
    raw  <- env.get(portKey)
    port <- raw.toIntOption
  yield s"$host:$port"

@main def run(): Unit =
  println(connString("DB_HOST", "DB_PORT"))
  println(connString("DB_HOST", "MISSING"))
  println(connString("DB_HOST", "BAD_PORT"))
Outputcompiled & run with real Scala
Some(db.local:5432)
None
None

Three chances to fail, no ifs and no nulls. The downside: None does not say which step failed. When the caller needs to know why, return an Either — next lesson.

Your turn

Add a fourth step to the for: user <- env.get("DB_USER"), and include it in the result as user@host:port.

Error you will hit

Using an Option[Int] where an Int is expected

scala
def withTax(price: Int): Int = price * 118 / 100

@main def run(): Unit =
  val prices = Map("tea" -> 20)
  println(withTax(prices.get("tea")))
-- [E007] Type Mismatch Error: Main.scala:5:28
5 |  println(withTax(prices.get("tea")))
  |                  ^^^^^^^^^^^^^^^^^
  |                  Found:    Option[Int]
  |                  Required: Int
1 error found
Compilation failed
Why the compiler said that

prices.get("tea") is an Option[Int]: the compiler knows the key might be missing and refuses to pretend it is an Int. This is the error that replaces a NullPointerException in production with a compile error on your laptop.

The fix

Either decide on a default before calling, or map the function over the Option and keep the result optional.

scala
def withTax(price: Int): Int = price * 118 / 100

@main def run(): Unit =
  val prices = Map("tea" -> 20)
  println(prices.get("tea").map(withTax))
  println(withTax(prices.getOrElse("tea", 0)))
03

Either: Left for errors, Right for results

Either[E, A] holds one of two things: Left(e) or Right(a). By convention Left is the error and Right is the result ("right" as in correct). Unlike None, a Left carries a value that explains what went wrong. Either is right-biased: map and flatMap work on the Right side and pass a Left through untouched.

scalaMain.scala
def parseAge(s: String): Either[String, Int] =
  s.toIntOption match
    case None                   => Left(s"'$s' is not a number")
    case Some(n) if n < 0       => Left(s"age cannot be negative: $n")
    case Some(n)                => Right(n)

@main def run(): Unit =
  println(parseAge("30"))
  println(parseAge("thirty"))
  println(parseAge("-4"))

  println(parseAge("30").map(_ + 1))
  println(parseAge("oops").map(_ + 1))
  println(parseAge("30").isRight)
  println(parseAge("oops").getOrElse(0))

  val msg = parseAge("-4").fold(err => s"Error: $err", age => s"Age $age")
  println(msg)
Outputcompiled & run with real Scala
Right(30)
Left('thirty' is not a number)
Left(age cannot be negative: -4)
Right(31)
Left('oops' is not a number)
true
0
Error: age cannot be negative: -4

map(_ + 1) added one to the Right and left the Left exactly as it was. fold takes two functions — one for each side — and is the usual way to turn an Either into a final message or HTTP response.

Your turn

Add a rule that ages over 150 are rejected with Left("age too large").

Right bias means for-comprehensions work on Either just like on Option, and now the first failure is kept. The error type is often a sealed trait or enum rather than a String, so callers can match on exactly which failure happened.

scalaMain.scala
enum TransferError:
  case AccountNotFound(id: String)
  case InsufficientFunds(needed: Int, available: Int)

import TransferError.*

val balances = Map("A" -> 100, "B" -> 20)

def balance(id: String): Either[TransferError, Int] =
  balances.get(id).toRight(AccountNotFound(id))

def transfer(from: String, to: String, amount: Int): Either[TransferError, String] =
  for
    fromBal <- balance(from)
    _       <- balance(to)
    _       <- if fromBal >= amount then Right(()) else Left(InsufficientFunds(amount, fromBal))
  yield s"moved $amount from $from to $to"

@main def run(): Unit =
  println(transfer("A", "B", 50))
  println(transfer("A", "Z", 50))
  println(transfer("B", "A", 50))
  transfer("B", "A", 50) match
    case Right(ok)                         => println(ok)
    case Left(AccountNotFound(id))         => println(s"no account $id")
    case Left(InsufficientFunds(n, have))  => println(s"short by ${n - have}")
Outputcompiled & run with real Scala
Right(moved 50 from A to B)
Left(AccountNotFound(Z))
Left(InsufficientFunds(50,20))
short by 30

toRight(error) converts an Option into an Either, using the given error for None. The third step returns Right(()) — "succeeded, nothing to report" — or a Left, which is how you put a check inside a for over Either.

Visualizetransfer("A", "Z", 50) stops at the first LeftStep 1 / 4
def transfer(from: String, to: String, amount: Int) =
for
fromBal <- balance(from)
_ <- balance(to)
_ <- if fromBal >= amount then Right(()) else Left(InsufficientFunds(amount, fromBal))
yield s"moved $amount from $from to $to"
Line 3

balance("A") finds 100 and returns Right(100). flatMap unwraps it and continues.

Variables now
fromBal100
All 4 steps as a table
StepLineWhat happenedVariables now
13balance("A") finds 100 and returns Right(100). flatMap unwraps it and continues.fromBal = 100
24balance("Z") finds nothing and returns Left(AccountNotFound(Z)).fromBal = 100 result = Left(AccountNotFound(Z))
35Skipped. flatMap on a Left does not call its function, so the balance check never runs.result = Left(AccountNotFound(Z))
46Skipped as well: the final map on a Left returns the Left unchanged. That Left is the whole result.result = Left(AccountNotFound(Z))
Error you will hit

An if guard inside a for over Either

scala
def parse(s: String): Either[String, Int] = s.toIntOption.toRight(s"bad: $s")

@main def run(): Unit =
  val r = for
    n <- parse("12")
    if n > 10
  yield n * 2
  println(r)
-- [E008] Not Found Error: Main.scala:5:9
5 |    n <- parse("12")
  |         ^^^^^^^^^^^
  |         value withFilter is not a member of Either[String, Int]
1 error found
Compilation failed
Why the compiler said that

A guard becomes withFilter. For an Option or a List, filtering out a value leaves None or an empty list. For an Either there is no sensible result — a filtered-out Right would have to become a Left, and the compiler has no error value to put in it — so Either simply has no withFilter.

The fix

Turn the check into a step that produces a Left with a real message. filterOrElse does exactly that.

scala
def parse(s: String): Either[String, Int] = s.toIntOption.toRight(s"bad: $s")

@main def run(): Unit =
  val r = for
    n <- parse("12").filterOrElse(_ > 10, "must be over 10")
  yield n * 2
  println(r)
04

Try: Success, Failure and recover

Plenty of existing code — Java libraries, toInt, file and network calls — reports failure by throwing an exception. Try { ... } runs a block and captures the outcome as a value: Success(result) or Failure(exception). From then on it works like Either, with the exception as the error: map, flatMap, getOrElse, for. recover turns selected failures back into successes.

scalaMain.scala
import scala.util.{Try, Success, Failure}

def divide(a: String, b: String): Try[Int] =
  Try(a.toInt / b.toInt)

@main def run(): Unit =
  println(divide("10", "2"))
  println(divide("10", "0"))
  println(divide("ten", "2"))

  println(divide("10", "0").getOrElse(-1))
  println(divide("10", "2").map(_ * 100))

  val safe = divide("10", "0").recover {
    case _: ArithmeticException => 0
  }
  println(safe)

  divide("ten", "2") match
    case Success(v) => println(s"got $v")
    case Failure(e) => println(s"failed: ${e.getMessage}")
Outputcompiled & run with real Scala
Success(5)
Failure(java.lang.ArithmeticException: / by zero)
Failure(java.lang.NumberFormatException: For input string: "ten")
-1
Success(500)
Success(0)
failed: For input string: "ten"

recover takes a partial function (Module 07): failures it matches become Success, anything else stays a Failure. Try catches only non-fatal exceptions — an OutOfMemoryError still escapes, which is what you want.

Your turn

Add a recover case for NumberFormatException that returns -1 and apply it to divide("ten", "2").

Try is for code that throws
Use Try at the edge, where you call something that throws. Do not write new functions that throw just so callers can wrap them in Try — return an Either with a proper error type instead. And like Option.get, Try.get rethrows the exception: it is not a way out.
05

Converting between Option, Either and Try

The three types are designed to convert into each other, so each layer of a program can use the one that fits. Going to Option always loses the error; going the other way you must supply one.

FromToMethodWhat happens to the failure
Option[A]Either[E, A]opt.toRight(err)You supply err for None
Either[E, A]Option[A]either.toOptionLeft becomes None; error dropped
Try[A]Either[Throwable, A]t.toEitherThe exception becomes the Left
Try[A]Option[A]t.toOptionException dropped
Either[Throwable, A]Try[A]either.toTryLeft(e) becomes Failure(e)
Either[E, A]Either[E2, A]either.left.map(f)Rewrites the error, keeps the result
scalaMain.scala
import scala.util.Try

@main def run(): Unit =
  val maybe: Option[Int] = None
  println(maybe.toRight("value missing"))
  println(Some(3).toRight("value missing"))

  val e: Either[String, Int] = Left("boom")
  println(e.toOption)

  val t = Try("42x".toInt)
  println(t.toOption)
  println(t.toEither.left.map(_.getClass.getSimpleName))
  println(Try("42".toInt).toEither)
Outputcompiled & run with real Scala
Left(value missing)
Right(3)
None
None
Left(NumberFormatException)
Right(42)

left.map is the one tool for working on the error side: here it turns a NumberFormatException into a short name a user could read.

06

When to throw and when to return Either

Scala still has throw and try/catch, and they are the right tool for some jobs. The dividing line is whether the caller is expected to handle the failure.

Return Either (or Option)

  • Invalid user input, a failed validation rule
  • A record that is not found
  • A business rule that says no (insufficient funds)
  • Anything the caller should decide how to handle
  • The failure is part of the function's type, so nobody forgets it

Throw an exception

  • A bug: a broken invariant, an impossible state (require, assert)
  • The program cannot sensibly continue: missing configuration at startup
  • Truly exceptional infrastructure failures a framework will catch and log
  • Inside a library where Java callers expect exceptions
  • Handled far away, not by the direct caller
scalaMain.scala
case class Money(cents: Long):
  require(cents >= 0, s"money cannot be negative: $cents")

def withdraw(balance: Money, amount: Money): Either[String, Money] =
  if amount.cents > balance.cents then Left("insufficient funds")
  else Right(Money(balance.cents - amount.cents))

@main def run(): Unit =
  println(withdraw(Money(500), Money(200)))
  println(withdraw(Money(100), Money(200)))

  try
    Money(-5)
  catch
    case e: IllegalArgumentException => println(s"bug caught: ${e.getMessage}")
Outputcompiled & run with real Scala
Right(Money(300))
Left(insufficient funds)
bug caught: requirement failed: money cannot be negative: -5

Not having enough money is a normal outcome, so withdraw returns Either. A negative Money can only come from a bug, so the constructor requires it and throws. require throws IllegalArgumentException with your message prefixed by "requirement failed".

07

A small validation pipeline

Here is the whole module working together on a realistic job: read raw order lines, validate each one, keep the good orders and report every bad line with its reason. Each rule is a small function returning Either; a for combines them per line; partitionMap splits the results into errors and orders in one pass.

scalaMain.scala
case class Order(id: Int, item: String, qty: Int)

def field(parts: Array[String], i: Int, name: String): Either[String, String] =
  parts.lift(i).map(_.trim).filter(_.nonEmpty).toRight(s"missing $name")

def positiveInt(s: String, name: String): Either[String, Int] =
  s.toIntOption.filter(_ > 0).toRight(s"$name must be a positive number, got '$s'")

def parseOrder(line: String): Either[String, Order] =
  val parts = line.split(",")
  for
    idRaw  <- field(parts, 0, "id")
    id     <- positiveInt(idRaw, "id")
    item   <- field(parts, 1, "item")
    qtyRaw <- field(parts, 2, "qty")
    qty    <- positiveInt(qtyRaw, "qty")
  yield Order(id, item, qty)

@main def run(): Unit =
  val lines = List("1,tea,3", "2,coffee,0", "x,milk,1", "4,,2", "5,sugar,10")
  val (errors, orders) = lines.zipWithIndex.partitionMap((line, i) =>
    parseOrder(line).left.map(err => s"line ${i + 1}: $err"))

  orders.foreach(println)
  errors.foreach(println)
  println(s"${orders.size} ok, ${errors.size} rejected")
Outputcompiled & run with real Scala
Order(1,tea,3)
Order(5,sugar,10)
line 2: qty must be a positive number, got '0'
line 3: id must be a positive number, got 'x'
line 4: missing item
2 ok, 3 rejected

parts.lift(i) is safe indexing: None instead of an ArrayIndexOutOfBoundsException. left.map adds the line number to each error. Within one line the checks stop at the first problem; across lines, every bad line is reported — nothing crashes, nothing is silently dropped.

Your turn

Add a rule that item must be one of Set("tea", "coffee", "milk", "sugar"), and add the line "6,juice,1" to test it.

In real jobs
This shape — parse, validate each record into Either, split into good and bad, send the bad ones to a dead-letter file or table — is how data pipelines handle messy input without failing a whole batch over one row. Libraries such as Cats add a Validated type that collects all errors in one record rather than just the first. See Data Engineering for where this pattern sits in a pipeline.
Option
A value that is either Some(value) or None; Scala's replacement for null.
Some / None
The two cases of Option: a present value, and no value.
getOrElse
Returns the value inside an Option, Either or Try, or a default if there is none.
fold
Handles both cases of an Option or Either in one call, with one function or value per case.
Either
A value that is Left(error) or Right(result); carries the reason for a failure.
Right-biased
map and flatMap on Either operate on the Right value and pass a Left through unchanged.
Try
The result of running code that may throw: Success(value) or Failure(exception).
recover
Turns matching Failures of a Try back into Successes using a partial function.
toRight
Converts an Option to an Either, supplying the Left value for None.
partitionMap
Splits a collection into two by a function returning Either: Lefts in one, Rights in the other.
Quick check

What does Left("bad").map((n: Int) => n * 2) return, typed as Either[String, Int]?

Quick check

A function looks up a customer by id, and "not found" is a normal outcome the caller must handle with a message. What should it return?

Frequently asked questions

What is the difference between Option and Either in Scala?
Option[A] says "a value may be missing" and nothing more: Some(a) or None. Either[E, A] says "this may fail, and here is why": Right(a) or Left(e). Use Option when absence is self-explanatory (a map lookup), and Either when the caller needs the reason.
Why should I avoid Option.get in Scala?
get throws NoSuchElementException on None, bringing back the runtime crash that Option exists to prevent. Use getOrElse, fold, map or a match, all of which make you handle the missing case.
When should I use Try instead of Either?
Use Try to wrap code that throws exceptions, typically a Java library or parsing call, and convert it with toEither once you know what the error means for your domain. For your own functions with expected failures, return Either with a specific error type.

Finish the Scala handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.