Blog

First class failure scenarios in Java

09 Dec, 2019
Xebia Background Header Wave

Checked exceptions were an effort by the designers of Java to express the possibility of failure in the type signature, aiding users of these methods in handling the failure scenario gracefully. While the intentions of the design were noble, the end result did not pan out as expected. Most Java programmers have dropped checked exceptions in favor of their unchecked counterparts. Contemporary Java allows us to express failure in the type signature in new and better ways. In this post I’ll explain why checked exceptions have fallen out of favor and what better approach there is to expressing the possibility of failure.

The Problem

Checked exceptions allow us to express potential failures in the type signature of our methods. Take the following method reading a file from disk:

String readFile(String filename) throws IOException

Any call-site of this API is now forced to explicitly handle the possibility of the requested file not existing. We enable the compiler to help the user of our API. So far so good.

A real world application however does not exist of a single method, but of the composition of many methods into a larger behavior. For instance we would like to parse an entity from the file we have just read from disk:

Entity parseEntity(String input) throw JsonParseException

Now we compose the result of our function to read a file, with our function to parse an entity:

parseEntity(readFile("entity.json"))

Checked exceptions are capable of handling this composition. To do this we need to wrap this line in a try-catch block with two separate catch blocks, one for each exception type:

try {
  parseEntity(readFile("entity.json"))
} catch (IOException e) {
  /* handle IOException */
} catch (JsonParseException e) {
  /* handle JsonParseException */
}

While not brilliantly elegant it is possible to express the composition of these two methods while using checked exceptions. Let’s push our example further. What if we need to read a list of filenames from disk and parse each as an entity. Using Java 8+ we would model this as a map operation over a stream:

streamOfFiles.map(this::readFile).map(this::parseEntity)

This example will not compile because the map operation can not handle methods which throw checked exceptions. This is revealed if we look at (a simplified) type signature of the map the method:

Stream<?> map(Function<?, ?> mapper)

By adding the failure scenario to the call signature of the method we have hampered our ability to reuse this method as a building block for bigger more complex behaviors.

The solution

One way we could resolve this problem is by converting our methods to throw unchecked exceptions. This way we can pass around our method by reference to other methods who could invoke them without any complaints from the compiler. By removing the failure scenario from our type signature we made the compiler can no longer aid users of this API in explicitly handling the possibility of failure.

What if, instead of relying on specialized syntax and semantics of checked exceptions, we could communicate the failure scenario as part of the standard signature. We could achieve this by returning a container value from our function representing either an error or a value:

Either<Exception, String> readFile(String filename)

The compiler can use this value to guide the user of our API to explicitly handle the scenario of failure.

By adding a map and flatMap operation on this container type we can compose other methods that either return a value or a new container over the value part of the Either container.Using these operations we can write our earlier composition of a single file as follows:

readFile("entity.json").flatMap(this::parseEntity)

The example of parsing multiple files can also be expressed using our either container value. It requires us to apply flatMap on the either values that are entries in our list:

streamOfFiles
  .map(this::readFile)
  .map(either -> either.flatMap(this::parseEntity))

The map and flatMap operation on the Either container only invoke if the target container contains a value. So if readFile fails, parseEntity will not be invoked and the IOException of readFile will bubble up the stream.

As a result of representing failure scenarios as common values instead of relying on specialized semantics. We can write combinators (i.e. methods that only operate on their input) encapsulating common behavior we want to express when dealing with failure scenarios.

For example we could write a method unwrapping the message from our failure scenario:

<R> Either<String, R> excToString(Either<Exception, R> either) {
  return either.mapLeft(Throwable::getMessage);
}

Or we could write a combinator that returns the first Either if it contains a value. But if it encapsulates a failure we return the second Either:

<L, R> Either<L, R> alternative(Either<L, R> first, Either<L, R> second) {
  return first.fold(l -> second, Either::right);
}

This relies on the Either.fold  method, which folds the left side or the the right side of the disjunction. Its API looks something like this:

<L, R, U> U fold(Function<L, U> leftMapper, Function<R, U> rightMapper)

We can use this combinator to use a fallback value value if our file entity_X.json doesn’t exist:

alternative(readFile("entity_X.json"), readFile("default_entity.json"));

(Beware that arguments passed to alternative are not interpreted lazily, thus this expression will try to load both files where only 1 will be used)

Conclusion

Similar to making functions first class citizens, making failure scenarios first class citizens allows you to write code abstracting over these concerns and combine them into new behaviors. Type-safe explicit failure scenarios supply you with both the benefits of checked exceptions, i.e. making failure scenarios explicit for the user, while granting you the composability of methods throwing unchecked exceptions. To start programming using the Either type you can use VAVR collections library which contains Either alongside many other useful collection and container types inspired by functional programming. If you are triggered by the ideas in this post and want to learn more I can recommend the talk “Railway Oriented Progamming”.

Matthisk Heimensen
A hands-on engineering consultant developing software across the stack. Helping teams deliver more predictably with more fun.
Questions?

Get in touch with us to learn more about the subject and related solutions

Explore related posts