Error Handling in Scala


Yuvi Masory - @ymasory

Scala for Startups

January 29, 2013



Slides live at http://yuvimasory.com/talks.

Section A:
Basic exception handling

Since Scala's try/catch blocks use pattern matching, this section is just a review of what patterns are available to you.

Consider this function

import scala.io.Source

def readFile(path: String) =
  (Source fromFile path).mkString()

A1. Catch an exception

import java.io.IOException

try {
  readFile("/opt/data")
}
catch {
  case e: IOException =>
}
  

A2. Catching multiple types

import java.lang.NumberFormatException

try {
  readFile("/opt/social-security").toInt
}
catch {
  case e @ (_: IOException | _: NumberFormatException) =>
}
  

A3. Subtyping exceptions

import java.nio.file.{ Files, FileSystemException,
                       FileSystems }

try {
  val path = FileSystems.getDefault getPath "/opt/data"
  Files readAllBytes path
}
catch {
  // abstracting over:
  //   - java.nio.file.NoSuchFileExceptions
  //   - java.nio.file.NotDirectoryException
  //   - all other subclasses
  case e: FileSystemException =>
}
  

A4. Catching exceptions by message

try {
  readFile("/opt/data").toInt
}
catch {
  case e: IOException
    if e.getMessage contains "Is a directory" =>
  case e: IOException =>
}
  

A5. Catching all exceptions

try {
  readFile("/opt/data").toInt
}
catch {
  case e: Exception =>
}
  

A6. DON'T DO THIS

try {
  readFile("/opt/data").toInt
}
catch {
  // this will match scala.util.control.ControlThrowable
  case _ =>
}
  

A7. Catching "everything"

try {
  readFile("/opt/data").toInt
}
catch {
  case t: ControlThrowable => throw t
  case _                   =>
}
  

Section B:
What's wrong with exceptions?

Referential transparency

Should I throw an exception?

Three possible answers

Section C:
Value-oriented exception handling

import scala.util.control._

Try[A] (slightly simplified)

sealed trait Try[A]

case class Failure[A](e: Throwable) extends Try[A]

case class Success[A](value: A)     extends Try[A]
  

C1. try/catch sans boilerplate

    import scala.util.Try

    def tryReadFile(path: String): Try[String] = Try {
      (Source fromFile path).mkString()
    }
  

C2. Matching on Try

    import scala.util.control._
    import scala.util.control.Exception._

    tryReadFile("/opt/data") match {
      case Success(value) =>
      case Failure(e)     =>
    }
  

Catch[A]

    val c: Catch[A] = catching(classOf[IOException])
  

C3. Converting to Option

    val sOpt: Option[String] =
      catching(classOf[IOException]).opt {
        readFile("/opt/data")
      }
  

C3. Converting to Option, cont ...

    val sOpt: Option[String] =
      failing(classOf[IOException]) {
        readFile("/opt/data")
      }
  

C4. Converting to Either

    val eith: Either[Throwable, String] =
      catching(classOf[IOException]).either {
        readFile("/opt/data")
      }
  

C5. Side-effecting

    val unit: Unit =
      ignoring(classOf[IOException], classOf[NumberFormatException]) {
        println(readFile("/opt/social-security").toInt)
      }
  

C6. DON'T DO THIS

    allCatch {
      readFile("/opt/data")
    }
  

C7. Catching "nonfatal" errors

    nonFatal {
      readFile("/opt/data")
    }
  

Which errors are "fatal"?

C7. Catching "nonfatal" errors, cont ...

    try {
      readFile("/opt/data")
    } catch {
      case NonFatal(e) =>
    }
  

C8. Provide a default value

    failAsValue(classOf[IOException])("") {
      readFile("/opt/data")
    }
  

C9. Custom error handling

(I'm still working on this one ...)

    def logError(t: Throwable): Int = {
      println("Error: " + t)
      -1
    }

    def loggingExceptions(block: => A) =
      catching(classOf[Exception]).withApply {
        logError _
      }.apply(block)
  

C10. What about finally?

Parting thoughts on scala.util.control

Section D:
Option[A]

Option[A] (slightly simplified)

sealed trait Option[A]

case class Some[A](a: A) extends Option[A]

case object None         extends Option[Nothing]
  

D1. Matching on Option

(Option[A], Option[A] => B) => B

    val iOpt: Option[Int] = //...

    val sOpt: Option[String] = iOpt match {
      case Some(value) => value.toString
      case None        => ""
    }
  

Stop matching on Option!!

D2. Only consider Some

(Option[A], A => B)) => Option[B]

    val sOpt: Option[String] =
      iOpt map { value => value.toString }

    val sOpt: Option[String] =
      iOpt map { _.toString }
  

D3. More chances

(Option[A], Option[A]) => Option[A]

    def foo(i: Int): Option[String]

    val sOpt: Option[String] =
      foo(1).orElse {
        foo(2).orElse {
          foo(3).orElse {
            foo(4)
          }
        }
      }
  

D4. Consider both cases

(Option[A], B, A => B)) => B

    val sOpt: Option[String] =
      iOpt.fold("") { _.toString }
  

D5. Provide default value

(Option[A], A) => A

    val i: Int = iOpt getOrElse 0
  

D6. Monads! (flatmap)

(Option[A], A => Option[B]) => Option[B]

    import scala.math.sqrt

    def perfectSqrt(i: Int): Option[Int] = {
      val root: Int = sqrt(i).toInt
      if (root * root == i) Some(root) else None
    }

    val i: Int = //...

    perfectSqrt(i).flatMap {
      perfectSqrt(_).flatMap {
        perfectSqrt(_)
      }
    }

  

D7. Monads! (for-comprehensions)

(Option[A], A => Option[B]) => Option[B]

    def triplePerfectSqrt(i: Int): Option[Int] = for {
      j <- perfectSqrt(i)
      k <- perfectSqrt(j)
      l <- perfectSqrt(k)
    } yield l
  

D8. Are you there?

Option[A] => Boolean

    val b1: Boolean = iOpt.isDefined

    val b2: Boolean = iOpt.isEmpty
  

D9. Are you there and ...?

(Option[A], A => Boolean) => Boolean

    val b1: Boolean = iOpt exists { _ > 0 }
  

D10. Dealing with null

A => Option[A]

    //MAY RETURN NULL
    def javaFunction(): String

    val sOpt: Option[String] = Option(javaFunction())
  

D11. Only execute if all the Options are defined

    val iOpt: Option[Int] = //...
    val cOpt: Option[Char] = //...
    val dOpt: Option[Double] = //...

    val sOpt: Option[String] = for {
      i <- iOpt
      c <- cOpt
      d <- dOpt
    } yield s"$i$s$d"
  

Section E:
Either[E, A]

Either[E, A] (slightly simplified)

sealed trait Either[E, A]

case class Left[E](e: E)  extends Either[E, A]

case class Right[A](a: A) extends Either[E, A]
  

E1: Pattern matching on Either

(Either[E, A], E => B, A => B) => B

    val eith: Either[Int, String] = //...

    eith match {
      case Left(e)      =>
      case Right(value) =>
    }
  

E2: Consider Left and Right both

(Either[E ,A], E => B, A => B) => B

    eith.fold(
      _.toString,
      _.reverse
    )
  

Where are all the Either methods?!

E3: Get the first failure, else proceed

    def div(i: Int, j: Int): Either[String, Int] =
      if (j != 0) Right(i / j)
      else Left(s"can't divide $i by 0!")

    val i, j, k, l, m, n = // ... integers

    for {
      i <- div(j, k).right
      l <- div(m, n).right
    } yield i + l
  

E3: Get the first success, or procceed

    for {
      i <- div(j, k).left
      l <- div(m, n).left
    } yield i + l
  

Section F:
Scalaz's Validation[E, A]

Validation[E, A] (slightly simplified)

sealed trait Validation[E, A]

case class Success[E, A](a: A) extends Validation[E, A]

case class Failure[E, A](e: E) extends Validation[E, A]
  

Some thoughts on Validation

Accumulating errors

ValidationNEL (slightly simplified)

case class NonEmptyList[A](head: A, tail: List[A])

type ValidationNEL[E, A] = Validation[NonEmptyList[E], A]
  

Consider this domain

class Person(
  age: Int, clothes: String,
  sober: Boolean, male: Boolean
)

def checkAge(p: Person): Validation[String, Person] =
  if (p.age < 18) Failure("too young!") else Success(p)

def checkClothes(p: Person): Validation[String, Person] =
  if (p.clothes contains "jeans") Failure("dress up!")
  else Success(p)

def checkSobriety(p: Person): Validation[String, Person] =
  if (p.sober) Success(p) else Failure("sober up!")
  

F1. Accumulate errors with applicative functors

def cost(p: Person): ValidationNEL[String, Int] = (
  checkAge(p).liftFailNel |@| 
  checkClothes(p).liftFailNel |@|
  checkSobriety(p).liftFailNel) {
    case (_, _, c) => if (c.male) 50 else 25
  }
 

Let's generalize

Aside:
type lambdas
(aka job security)

Read this! What are type lambdas in Scala and what are their benefits?

Type systems

Example: Can Either[E, A] be a Monad?

Partially applying types

Huh?

Let's build that one up step by step.

Once again.

/Aside

F2. Accumulate errors with traverse

def cost(p : Person) : ValidationNEL[String, Int] = {
  val checks = List(
    checkAge _,
    checkClothes _,
    checkSobriety _
  )
  checks.traverse[({type λ[α] = ValidationNEL[String, α]})#λ, Person](
    _ andThen (_.liftFailNel) apply p
  ) map {
    case c :: _ => if (c.male) 50 else 25
  }
}
 

Section G:
Lift's Box[A]

Box[A] (slightly simplified)

sealed trait Box[A]

case class Full[A](a: A) extends Box[A]

abstract class EmptyBox  extends Box[Nothing]

case class Failure(
  msg: String, e: Box[Throwable], chain: Box[Failure]
) extends EmptyBox

case class Empty extends EmptyBox
  

Sorry!

Phew!

The end.

/

#