Typeful error handling with higher-kinded types

Previously we looked at a type class which we called SafeSerializable. We used it as a type-safe, composable way to serialize our data types. We closed with an observation that, while our happy path is quite type-safe, our error handling is not. Currently there is nothing about the type signatures of our serialize and deserialize methods that indicate that things could go wrong. Let's explore the options for changing that.

Returning a Try

One of the most straightforward ways in Scala to lift your error handling from raw exception throwing up to the type level is to use the standard library’s Try type. Try is like Option, if None carried an exception. By returning a Try, we inform our caller that an error may have occurred, and they need to deal with it. Here is how this would look:

Now our callers will need acknowledge and handle the possibility of an error in order to get to the successful result. They could of course call .get on the resulting Try, but then we've still forced an explicit decision to ignore possible errors, which is still a win.

One shortcoming of Try is that, when you have a failure, you have to put an exception inside. If you instead have some actual error types, and would rather return them without raising an Exception, there is also the Either type. Either is the more traditional functional way of handling errors, and Try is a nice compromise when you run in the JVM and Exceptions are everywhere.

Using Either with an explicit error type has the advantage of making it much easier to pattern match on all possible errors; this is less straightforward when all you have is a Throwable. Here is what this might look like:

You get the point. Besides Either and Try, we could also decide that serialization is taking a very long time, and that we don't want to block, and start returning our results inside Futures. Maybe you love cats.Validated (it is pretty great) and would prefer to use it here instead of Either. In fact, there are many types in Scala that one might want to choose from to represent the possible 'effects' of serialization.

Instead of choosing just one, or worse, implementing 10 copies of SafeSerializable with a different effect type each time, let's see if we can make our type class work with all effect types.

The thing that all of these ‘effect types’ have in common is that they take a type parameter which represents the type of value they contain (or may contain). You can think of them as containers or wrappers. When we want a ‘type that takes another type’ in a function, we use the syntax F[_]. This is actually a higher-kinded type parameter. What we're saying is pretty simple, though: you can implement SafeSerializable with Option[A], Try[A], or Future[A] - it doesn't really matter as long as the effect type you give takes a single type parameter.

Here is our new type class, now generalized, with convenience constructors for two effect types: Try and Id. (Id stands for identity and can be thought of as wrapping a value)

  • Id[A] is like a wrapper for A that does not need to be unboxed - a handy trick.
  • makeTry returns an instance of SafeSerializable that wraps its results in Try
  • make replaces our original make from last time by using the Id effect type

With our type class defined, let’s re-define our convenient syntax for invoking its functionality.

Note that this time, serialize and deserialize take an additional type parameter: the same F[_]. They must know about F since it is their new return type!

Now we’re ready to handle some serialization errors. Let’s set up our test class again with a basic serialize and deserialize function:

Time to try it out — let’s start with a Try-based serializer:

We were forced to pattern match on result to get at our bytes, handling possible errors in the process. However, if we wanted to pretend we had our old, dangerous version of SafeSerializable back, we'd just use an Id-based serializer:

And there you have it. We’re returning error-aware types now from our serialize and deserialize calls. We’ve significantly increased the generality of our type class by allowing it to return a ‘wrapped’ value, or an effect type.

We haven’t coupled SafeSerializable to any particular style of error or effect handling, we've simply provided an interface to which a consumer could 'plug in' their own effect types.

Further reading

The technique of abstracting over an effect type or monad has been called “Finally Tagless Encoding”. I find the term confusing and awkward so I didn’t use it here, but for a deeper dive into the concept, that’s the search term to use! In particular, this post by Adam Warski is excellent for familiarizing yourself with these types of patterns, and working with monads in general.

Originally published at gist.github.com.