scalalift

Why Box/Option instead of Exception in LiftWeb/Scala?


So, I've been reading this article about Box usage in LiftWeb which seems so be part of the official documentation as it's linked through the source code comments. My question is why is Box/Failure preferable to actually coding without null and throwing an Exception that would be caught at top level and transformed into an appropriate error code/message. So instead of

case "user" :: "info" :: _ XmlGet _ =>
  for {
    id <- S.param("id") ?~ "id param missing" ~> 401
    u <- User.find(id) ?~ "User not found"
  } yield u.toXml

why not

case "user" :: "info" :: _ XmlGet _ => User.find(S.param("id").openOrThrow(
    new IllegalArgumentException("idParamMissing"))).toXml

and have User.find throw something like NotFoundException


Solution

  • Imagine you have a method which does some operation which may potentially fail, for example fetching a web page.

    def getUrl(url: String): NodeSeq = {
       val page = // ...
       // ...
       if (failure) throw new PageNotFoundException()
       page
    }
    

    If you want to use it, you need to do

    val node = try {
      getUrl(someUrl)
    } catch {
      case PageNotFoundException => NodeSeq.Empty
    }
    

    or similar depending on the situation. Still, it looks somewhat okay to do so. But now imagine you want to do this for a collection of URLs.

    val urls = Seq(url1, ...)
    val nodeseqs: Seq[NodeSeq] = try {
      urls.map(getUrl)
    } catch {
      case PageNotFoundException => Seq.Empty
    }
    

    Okay so this return an empty sequence whenever one of the pages could not be loaded. What if we’d like to receive as many as possible?

    val urls = Seq(url1, ...)
    val nodeseqs: Seq[NodeSeq] = urls.map { url =>
      try {
        getUrl(url)
      } catch {
        case PageNotFoundException => NodeSeq.Empty
      }
    }
    

    Now our logic is intermingled with error handling code.

    Compare this to the following:

    def getUrl(url: String): Box[NodeSeq] = {
      val page = // ...
      // ...
      if (failure) Failure("Not found")
      else Full(page)
    }
    
    val urls = Seq(url1, ...)
    val nodeseqs: Seq[Box[NodeSeq]] = urls.map(getUrl(url)).filter(_.isDefined)
    // or even
    val trueNodeseqs: Seq[NodeSeq] = urls.map(getUrl(url)).flatten
    

    Using Option or Box (or Either or scalaz’ Validation) gives you way more power over deciding when to deal with a problem than throwing exceptions ever could.

    With exceptions you may only traverse the stack and catch it as some point there. If you encode the failure inside a type, you may carry it around with you as long as you like and deal with it in the situation you think is most appropriate.