Cristian Vrabie
Cristian Vrabie

Reputation: 4068

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

Upvotes: 2

Views: 483

Answers (2)

Debilski
Debilski

Reputation: 67828

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.

Upvotes: 2

Alexey Romanov
Alexey Romanov

Reputation: 170713

If you throw an exception, the only thing you can do is catch it. So, if two parts of your page throw an exception, you won't ever get to the second one. If one part of your page returns a Box which happens to be a Failure, and the second part doesn't need the return value of the first one, you can see both. More generally, Box has a useful API which Exception lacks.

Upvotes: 2

Related Questions