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
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.