Error Handling
## Learning Objectives
- Master Option type for null safety
- Work with Try for exception handling
- Use Either for error messages
- Understand for comprehensions with error types
- Learn best practices for error handling
## Option
Option represents a value that may or may not exist:
```scala
val someValue: Option[Int] = Some(5)
val noValue: Option[Int] = None
```
### Creating Options
```scala
// From nullable value
val name: String = null
val optionName: Option[String] = Option(name) // None
val name2: String = "Alice"
val optionName2: Option[String] = Option(name2) // Some("Alice")
// From Map
val map = Map("a" -> 1, "b" -> 2)
map.get("a") // Some(1)
map.get("c") // None
```
### Extracting Values
```scala
val option: Option[Int] = Some(42)
// Pattern matching
option match {
case Some(value) => println(s"Got: $value")
case None => println("No value")
}
// getOrElse
val result = option.getOrElse(0) // 42
val noResult = None.getOrElse(0) // 0
// orElse
val alt = None.orElse(Some(10)) // Some(10)
// isDefined, isEmpty
option.isDefined // true
option.isEmpty // false
```
### Map on Option
```scala
val option: Option[Int] = Some(5)
option.map(_ * 2) // Some(10)
option.map(_ + 10) // Some(15)
None.map(_ * 2) // None
// FlatMap on Option
def parseInt(s: String): Option[Int] = {
try Some(s.toInt)
catch case _: Exception => None
}
Some("42").flatMap(parseInt) // Some(42)
Some("abc").flatMap(parseInt) // None
None.flatMap(parseInt) // None
```
### Filtering
```scala
val option: Option[Int] = Some(5)
option.filter(_ > 10) // None (5 not > 10)
option.filter(_ > 3) // Some(5)
// withFilter (doesn't wrap in Option)
option.withFilter(_ > 10).foreach(println) // No output
```
## Try
Try represents computation that may throw an exception:
```scala
import scala.util.{Success, Failure, Try}
val success: Try[Int] = Success(42)
val failure: Try[Int] = Failure(new Exception("Oops"))
```
### Creating Try
```scala
import scala.util.Try
// From code that might throw
def parseInt(s: String): Try[Int] = Try(s.toInt)
parseInt("42") // Success(42)
parseInt("abc") // Failure(NumberFormatException)
// Using Try with Option
def safeDivision(a: Int, b: Int): Try[Int] = Try(a / b)
safeDivision(10, 2) // Success(5)
safeDivision(10, 0) // Failure(ArithmeticException)
```
### Try Extracting Values
```scala
val tryValue: Try[Int] = Try(42 / 2)
// Pattern matching
tryValue match {
case Success(v) => println(s"Got: $v")
case Failure(ex) => println(s"Error: ${ex.getMessage}")
}
// getOrElse
tryValue.getOrElse(0) // 21
// orElse
def failing: Try[Int] = Try(throw new Exception("Oops"))
failing.orElse(Success(0)) // Success(0)
```
### Map and FlatMap on Try
```scala
val tryValue: Try[Int] = Success(5)
tryValue.map(_ * 2) // Success(10)
tryValue.map(_ + 10) // Success(15)
Failure(new Exception("Oops")).map(_ * 2) // Failure
// FlatMap
def safeSquare(x: Int): Try[Int] = Try {
if (x > 10) throw new Exception("Too large")
else x * x
}
Success(5).flatMap(safeSquare) // Success(25)
Success(15).flatMap(safeSquare) // Failure
```
### Recover
```scala
def parseInt(s: String): Try[Int] = Try(s.toInt)
parseInt("abc").recover {
case _: NumberFormatException => 0
} // Success(0)
parseInt("abc").recoverWith {
case _: NumberFormatException => Try(42)
} // Success(42)
// Recover with partial function
parseInt("abc").recoverWith {
case _: NumberFormatException => Success(0)
case _: Exception => Failure(...)
} // Success(0)
```
## Either
Either represents one of two types:
```scala
val left: Either[String, Int] = Left("Error")
val right: Either[String, Int] = Right(42)
```
### Creating Either
```scala
def parseInt(s: String): Either[String, Int] = {
try Right(s.toInt)
catch case e: Exception => Left(e.getMessage)
}
parseInt("42") // Right(42)
parseInt("abc") // Left("For input string: abc")
```
### Either Extracting Values
```scala
val either: Either[String, Int] = Right(42)
// Pattern matching
either match {
case Right(v) => println(s"Got: $v")
case Left(msg) => println(s"Error: $msg")
}
// getOrElse (only on Right)
either.right.getOrElse(0) // 42
Left("error").right.getOrElse(0) // 0
// Swap
either.swap // Left(42) - not useful here
```
### Map on Either
```scala
val right: Either[String, Int] = Right(5)
right.map(_ * 2) // Right(10)
right.map(_ + 10) // Right(15)
Left("error").map(_ * 2) // Left("error") - preserved
// FlatMap
def safeSquare(x: Int): Either[String, Int] = {
if (x > 10) Left("Too large")
else Right(x * x)
}
Right(5).flatMap(safeSquare) // Right(25)
Right(15).flatMap(safeSquare) // Left("Too large")
```
### for Comprehensions with Either
```scala
def parseInt(s: String): Either[String, Int] =
try Right(s.toInt)
catch case e: Exception => Left(e.getMessage)
for {
a <- parseInt("10")
b <- parseInt("20")
c <- parseInt("30")
} yield a + b + c // Right(60)
for {
a <- parseInt("10")
b <- parseInt("abc") // This fails
c <- parseInt("30")
} yield a + b + c // Left("For input string: abc")
```
## For Comprehensions with Option
```scala
case class User(name: String, email: Option[String])
def getUser(id: Int): Option[User] = ???
def getEmailDomain(email: String): Option[String] = ???
val domain = for {
user <- getUser(1)
email <- user.email
domain <- getEmailDomain(email)
} yield domain
// Without for comprehension
val domainAlt = getUser(1).flatMap { user =>
user.email.flatMap { email =>
getEmailDomain(email)
}
}
```
## Chaining Operations
### Using fold
```scala
def parseInt(s: String): Option[Int] =
Try(s.toInt).toOption
List("1", "2", "abc", "4")
.map(parseInt)
.fold(0)(_ + _) // 0 + 1 + 2 + 0 = 3 (abc becomes 0)
// Or using collect
List("1", "2", "abc", "4")
.map(parseInt)
.collect { case Some(n) => n }
.sum // 7
```
### Using sequence
```scala
import scala.util.TryingAnd Sequencing._
val options: List[Option[Int]] = List(Some(1), Some(2), Some(3))
val noneOptions: List[Option[Int]] = List(Some(1), None, Some(3))
options.sequence // Some(List(1, 2, 3))
noneOptions.sequence // None
// traverse
def parseInt(s: String): Option[Int] = Try(s.toInt).toOption
List("1", "2", "3").traverse(parseInt) // Some(List(1, 2, 3))
List("1", "abc", "3").traverse(parseInt) // None
```
## Best Practices
1. **Prefer Option over null**
2. **Use Try for code that throws**
3. **Use Either for expected failures with messages**
4. **Chain operations instead of nested if-else**
5. **Use for comprehensions for clean error handling**
6. **Don't catch Exception unnecessarily**
7. **Return meaningful error messages in Either**
## Summary
- Option handles optional values (Some or None)
- Try handles exceptions (Success or Failure)
- Either handles expected failures (Left for error, Right for success)
- map, flatMap, filter work on these types
- for comprehensions provide clean syntax for chaining
- Use getOrElse for providing default values
- recover and recoverWith handle failures
- sequence and traverse work with collections of error types
Comments
Comments powered by Giscus
To enable comments, add your Giscus embed code here.
Learn more about Giscus →