Functional Error Handling in Scala
Long Cao
Software Engineer @ MediaMath
What is error handling?
- In Java, usually means exceptions and try/catch
- Java has checked exceptions - compiler forces handling (pretty flawed, though)
- Scala has only unchecked exceptions - compiler won't complain about it
class Coffee
def buyCoffee(money: Int): Coffee = {
if (money < 3)
throw new Exception(s"not enough money, need 3 dollars")
else
new Coffee
}
What is functional error handling?
- In Scala, that means using error handling types and passing in functions into their contexts
- Referential transparency - replace expression with its value predictably
- Correctness and easier to understand quickly
- Bonuses - compiler optimizations, memoization
class Coffee
// Not referentially transparent:
// Does this function always return a Coffee if it throws an exception?
def buyCoffee(money: Int): Coffee
What I hope you get out of this
- Use error handling types to wrap over program flow: compiler guarantees
- Don't throw exceptions, breaks referential transparency
- If it flatMaps, "business logic" becomes functions that operate on your error handling monads - very testable, especially if stateless and pure
- A practical application of functional error handling- you don't have to dive too far into the deep end or read/write byzantine code to reap the benefits
A vastly oversimplified explanation of 'monad'
- A 'container' type that has predefined behavior
- Allows for building computations - chain and compose of functions
- It's a box!
So... how do we go about making it better?
How to improve using Option
- Use Options to indicate found/not found states instead of throwing exceptions
- Q: Do the types tell you anything about why it failed?
def buyCoffee(money: Int): Option[Coffee] = {
if (money < 3)
None
else
Some(new Coffee)
}
scala> buyCoffee(2).flatMap(_ => Some("i got a coffee"))
res0: Option[String] = None
scala> buyCoffee(3).flatMap(_ => Some("i got a coffee"))
res2: Option[String] = Some(i got a coffee)
How to improve using Try
- Represents success or failure states
- Useful for mingling with/wrapping over exception-throwing code
import scala.util.{ Failure, Success, Try }
def buyCoffee(money: Int): Try[Coffee] = {
Try {
if (money < 3)
throw new Exception(s"not enough money, need 3 dollars")
else
new Coffee
}
}
scala> buyCoffee(2)
res0: scala.util.Try[Coffee] =
Failure(java.lang.Exception: not enough money, need 3 dollars)
scala> buyCoffee(3)
res1: scala.util.Try[Coffee] = Success(Coffee@5e7cd6cc)
How to improve using Either
- Disjoint union - return one of two wrapped types
- By convention: right-biased, Left indicates error, Right: success
- Can't flatMap over it out-of-the-box: not a monad
case class FailureReason(reason: String)
def buyCoffee(money: Int): Either[FailureReason, Coffee] = {
if (money < 3)
Left(FailureReason(s"not enough money, need 3 dollars"))
else
Right(new Coffee)
}
scala> buyCoffee(2)
res0: scala.util.Either[FailureReason,Coffee] =
Left(FailureReason(not enough money, need 3))
scala> buyCoffee(3)
res1: scala.util.Either[FailureReason,Coffee] =
Right(Coffee@2a8448fa)
Library Alternatives
- Disjoint Unions (similar to Either)
- scalaz.\/
- Right-biased disjunction
- org.scalactic.Or
- Left-biased
- Reads as the "Good value Or Bad value"
- cats.data.Xor
- Right-biased disjunction
- scalaz.\/
Common motif: Fail-fast error handling
- Return the first error encountered in the flow when flatmapping over it
- Kind of like an unhandled exception except the compiler will complain if you don't handle your cases
- Most of these container types mentioned can be weaved together with for-comprehensions - will still fail fast!
For-comprehension with Or
import org.scalactic.{ Bad, Good, Or }
object CoffeeServiceOr {
def purchaseCoffee(money: Int): Coffee Or FailureReason =
for {
beans <- buyBeans(money)
coffee <- brewCoffee(beans)
} yield coffee
// fails without enough money
def buyBeans(money: Int): Beans Or FailureReason = { ... }
// fails 25% of the time to simulate a faulty grinder
def brewCoffee(beans: Beans): Coffee Or FailureReason = { ... }
}
scala> CoffeeServiceOr.purchaseCoffee(2)
res0: org.scalactic.Or[Coffee,FailureReason] =
Bad(FailureReason(Not enough money to buy beans for a coffee, need 3))
scala> CoffeeServiceOr.purchaseCoffee(3)
res1: org.scalactic.Or[Coffee,FailureReason] =
Bad(FailureReason(Faulty grinder failed to grind beans!))
Caveats; things to think about
- Mind your boundaries
- Push final decision making with errors to the edges of your code
- Be mindful of APIs exposed to clients - not everyone would know what to do with a scalaz.\/
- Hard dependencies - scalaz, scalactic, & cats - be careful if your team isn't ready
- Performance hits possible - lots of allocations, but measure before you conclude
Suggested Reading
- Functional Programming in Scala - Chapter 4: "Handling errors without exceptions"
Questions?
Functional Error Handling in Scala
By longcao
Functional Error Handling in Scala
- 1,686