The Neophyte's Guide to Scala Part 6: Error Handling With Try
When just playing around with a new language, you might get away with simply ignoring the fact that something might go wrong. As soon you want to create anything serious, though, you can no longer run away from handling errors and exceptions in your code. The importance of how well a language supports you in doing so is often underestimated, for some reason or another.
Scala, as it turns out, is pretty well positioned when it comes to dealing with error conditions in an elegant way. In this article, I’m going to present Scala’s approach to dealing with errors, based on the Try
type, and the rationale behind it. I’m using features introduced with Scala 2.10 and ported back to Scala 2.9.3, so make sure your Scala version in SBT is at 2.9.3 or later.
Throwing and catching exceptions
Before going straight to Scala’s idiomatic approach at error handling, let’s first have a look at an approach that is more akin to how you are used to working with error conditions if you come from languages like Java or Ruby. Like these languages, Scala allows you to throw an exception:
case class Customer(age: Int)
class Cigarettes
case class UnderAgeException(message: String) extends Exception(message)
def buyCigarettes(customer: Customer): Cigarettes =
if (customer.age < 16)
throw UnderAgeException(s"Customer must be older than 16 but was ${customer.age}")
else new Cigarettes
Thrown exceptions can be caught and dealt with very similarly to Java, albeit using a partial function to specify the exceptions we want to deal with. Also, Scala’s try
/catch
is an expression, so the following code returns the message of the exception:
val youngCustomer = Customer(15)
try {
buyCigarettes(youngCustomer)
"Yo, here are your cancer sticks! Happy smokin'!"
} catch {
case UnderAgeException(msg) => msg
}
Error handling, the functional way
Now, having this kind of exception handling code all over your code base can become ugly very quickly and doesn’t really go well with functional programming. It’s also a rather bad solution for applications with a lot of concurrency. For instance, if you need to deal with an exception thrown by an Actor that is executed on some other thread, you obviously cannot do that by catching that exception – you will want a possibility to receive a message denoting the error condition.
Hence, in Scala, it’s usually preferred to signify that an error has occurred by returning an appropriate value from your function.
Don’t worry, we are not going back to C-style error handling, using error codes that we need to check for by convention. Rather, in Scala, we are using a specific type that represents computations that may result in an exception.
In this article, we are confining ourselves to the Try
type that was introduced in Scala 2.10 and later backported to Scala 2.9.3. There is also a similar type, called Either
, which, even after the introduction of Try
, can still be very useful, but is more general.
The semantics of Try
The semantics of Try
are best explained by comparing them to those of the Option
type that was the topic of the previous part of this series.
Where Option[A]
is a container for a value of type A
that may be present or not, Try[A]
represents a computation that may result in a value of type A
, if it is successful, or in some Throwable
if something has gone wrong. Instances of such a container type for possible errors can easily be passed around between concurrently executing parts of your application.
There are two different types of Try
: If an instance of Try[A]
represents a successful computation, it is an instance of Success[A]
, simply wrapping a value of type A
. If, on the other hand, it represents a computation in which an error has occurred, it is an instance of Failure[A]
, wrapping a Throwable
, i.e. an exception or other kind of error.
If we know that a computation may result in an error, we can simply use Try[A]
as the return type of our function. This makes the possibility explicit and forces clients of our function to deal with the possibility of an error in some way.
For example, let’s assume we want to write a simple web page fetcher. The user will be able to enter the URL of the web page they want to fetch. One part of our application will be a function that parses the entered URL and creates a java.net.URL
from it:
import scala.util.Try
import java.net.URL
def parseURL(url: String): Try[URL] = Try(new URL(url))
As you can see, we return a value of type Try[URL]
. If the given url
is syntactically correct, this will be a Success[URL]
. If the URL
constructor throws a MalformedURLException
, however, it will be a Failure[URL]
.
To achieve this, we are using the apply
factory method on the Try
companion object. This method expects a by-name parameter of type A
(here, URL
). For our example, this means that the new URL(url)
is executed inside the apply
method of the Try
object. Inside that method, non-fatal exceptions are caught, returning a Failure
containing the respective exception.
Hence, parseURL("http://danielwestheide.com")
will result in a Success[URL]
containing the created URL, whereas parseURL("garbage")
will result in a Failure[URL]
containing a MalformedURLException
.
Working with Try values
Working with Try
instances is actually very similar to working with Option
values, so you won’t see many surprises here.
You can check if a Try
is a success by calling isSuccess
on it and then conditionally retrieve the wrapped value by calling get
on it. But believe me, there aren’t many situations where you will want to do that.
It’s also possible to use getOrElse
to pass in a default value to be returned if the Try
is a Failure
:
val url = parseURL(Console.readLine("URL: ")) getOrElse new URL("http://duckduckgo.com")
If the URL given by the user is malformed, we use the URL of DuckDuckGo as a fallback.
Chaining operations
One of the most important characteristics of the Try
type is that, like Option
, it supports all the higher-order methods you know from other types of collections. As you will see in the examples to follow, this allows you to chain operations on Try
values and catch any exceptions that might occur, and all that in a very readable manner.
Mapping and flat mapping
Mapping a Try[A]
that is a Success[A]
to a Try[B]
results in a Success[B]
. If it’s a Failure[A]
, the resulting Try[B]
will be a Failure[B]
, on the other hand, containing the same exception as the Failure[A]
:
parseURL("http://danielwestheide.com").map(_.getProtocol)
// results in Success("http")
parseURL("garbage").map(_.getProtocol)
// results in Failure(java.net.MalformedURLException: no protocol: garbage)
If you chain multiple map
operations, this will result in a nested Try
structure, which is usually not what you want. Consider this method that returns an input stream for a given URL:
import java.io.InputStream
def inputStreamForURL(url: String): Try[Try[Try[InputStream]]] = parseURL(url).map { u =>
Try(u.openConnection()).map(conn => Try(conn.getInputStream))
}
Since the anonymous functions passed to the two map
calls each return a Try
, the return type is a Try[Try[Try[InputStream]]]
.
This is where the fact that you can flatMap
a Try
comes in handy. The flatMap
method on a Try[A]
expects to be passed a function that receives an A
and returns a Try[B]
. If our Try[A]
instance is already a Failure[A]
, that failure is returned as a Failure[B]
, simply passing along the wrapped exception along the chain. If our Try[A]
is a Success[A]
, flatMap
unpacks the A
value in it and maps it to a Try[B]
by passing this value to the mapping function.
This means that we can basically create a pipeline of operations that require the values carried over in Success
instances by chaining an arbitrary number of flatMap
calls. Any exceptions that happen along the way are wrapped in a Failure
, which means that the end result of the chain of operations is a Failure
, too.
Let’s rewrite the inputStreamForURL
method from the previous example, this time resorting to flatMap
:
def inputStreamForURL(url: String): Try[InputStream] = parseURL(url).flatMap { u =>
Try(u.openConnection()).flatMap(conn => Try(conn.getInputStream))
}
Now we get a Try[InputStream]
, which can be a Failure
wrapping an exception from any of the stages in which one may be thrown, or a Success
that directly wraps the InputStream
, the final result of our chain of operations.
Filter and foreach
Of course, you can also filter a Try
or call foreach
on it. Both work exactly as you would expect after having learned about Option
.
The filter
method returns a Failure
if the Try
on which it is called is already a Failure
or if the predicate passed to it returns false
(in which case the wrapped exception is a NoSuchElementException
). If the Try
on which it is called is a Success
and the predicate returns true
, that Succcess
instance is returned unchanged:
def parseHttpURL(url: String) = parseURL(url).filter(_.getProtocol == "http")
parseHttpURL("http://apache.openmirror.de") // results in a Success[URL]
parseHttpURL("ftp://mirror.netcologne.de/apache.org") // results in a Failure[URL]
The function passed to foreach
is executed only if the Try
is a Success
, which allows you to execute a side-effect. The function passed to foreach
is executed exactly once in that case, being passed the value wrapped by the Success
:
parseHttpURL("http://danielwestheide.com").foreach(println)
For comprehensions
The support for flatMap
, map
and filter
means that you can also use for comprehensions in order to chain operations on Try
instances. Usually, this results in more readable code. To demonstrate this, let’s implement a method that returns the content of a web page with a given URL using for comprehensions.
import scala.io.Source
def getURLContent(url: String): Try[Iterator[String]] =
for {
url <- parseURL(url)
connection <- Try(url.openConnection())
is <- Try(connection.getInputStream)
source = Source.fromInputStream(is)
} yield source.getLines()
There are three places where things can go wrong, all of them covered by usage of the Try
type. First, the already implemented parseURL
method returns a Try[URL]
. Only if this is a Success[URL]
, we will try to open a connection and create a new input stream from it. If opening the connection and creating the input stream succeeds, we continue, finally yielding the lines of the web page. Since we effectively chain multiple flatMap
calls in this for comprehension, the result type is a flat Try[Iterator[String]]
.
Please note that this could be simplified using Source#fromURL
and that we fail to close our input stream at the end, both of which are due to my decision to keep the example focussed on getting across the subject matter at hand.
Pattern Matching
At some point in your code, you will often want to know whether a Try
instance you have received as the result of some computation represents a success or not and execute different code branches depending on the result. Usually, this is where you will make use of pattern matching. This is easily possible because both Success
and Failure
are case classes.
We want to render the requested page if it could be retrieved, or print an error message if that was not possible:
import scala.util.Success
import scala.util.Failure
getURLContent("http://danielwestheide.com/foobar") match {
case Success(lines) => lines.foreach(println)
case Failure(ex) => println(s"Problem rendering URL content: ${ex.getMessage}")
}
Recovering from a Failure
If you want to establish some kind of default behaviour in the case of a Failure
, you don’t have to use getOrElse
. An alternative is recover
, which expects a partial function and returns another Try
. If recover
is called on a Success
instance, that instance is returned as is. Otherwise, if the partial function is defined for the given Failure
instance, its result is returned as a Success
.
Let’s put this to use in order to print a different message depending on the type of the wrapped exception:
import java.net.MalformedURLException
import java.io.FileNotFoundException
val content = getURLContent("garbage") recover {
case e: FileNotFoundException => Iterator("Requested page does not exist")
case e: MalformedURLException => Iterator("Please make sure to enter a valid URL")
case _ => Iterator("An unexpected error has occurred. We are so sorry!")
}
We could now safely get
the wrapped value on the Try[Iterator[String]]
that we assigned to content
, because we know that it must be a Success
. Calling content.get.foreach(println)
would result in Please make sure to enter a valid URL
being printed to the console.
Conclusion
Idiomatic error handling in Scala is quite different from the paradigm known from languages like Java or Ruby. The Try
type allows you to encapsulate computations that result in errors in a container and to chain operations on the computed values in a very elegant way. You can transfer what you know from working with collections and with Option
values to how you deal with code that may result in errors – all in a uniform way.
To keep this article at a reasonable length, I haven’t explained all of the methods available on Try
. Like Option
, Try
supports the orElse
method. The transform
and recoverWith
methods are also worth having a look at, and I encourage you to do so.
In the next part we are going to deal with Either
, an alternative type for representing computations that may result in errors, but with a wider scope of application that goes beyond error handling.
Posted by Daniel Westheide