Dysfunctional programming in Java 5 : No Exceptions

From dysfunctional to functional

John McClean
11 min readNov 1, 2018

So far in this series, of functional programming in Java Tutorials, we’ve introduced 2 constraints that will significantly improve the quality of our code and now we introduce a third radical rule.

  1. 🏔Make your data model immutable 🏔
  2. 🚫Do not permit null assignments🚫
  3. 🚫🆕Do not throw Exceptions🆕🚫
Photo by Noah Buscher on Unsplash

Banning the throwing of Exceptions in your own code, will lead to better designs by forcing engineers to create Object models that ensure illegal states aren’t representable in code. There are parts of our applications that deal with tasks that can neccessarily fail (I/O to external systems) and there we can encode error states neatly as values, rather than exceptional disruptions to program control flow.

(⬇️️ Continued below ️⬇️️)

Outline

We will cover the following concepts

  • Why consider banning Exceptions?
  • The simplicity of pure functions
  • Converting methods that throw Exceptions to pure form
  • Integrating with code that throws Exceptions (via Try)
  • Managing I/O classes that throw Exceptions and require Resources to be closed

Why Ban Exceptions?

Exceptions increase the complexity of our code. In Java they can either be visible on the signatures of the methods we call (CheckedExceptions) or entirely invisible. Each style presents their own issues.

Exceptions disrupt the flow of execution in our applications, jumping from the point at which the exception is thrown to whatever point, with whatever state, our application defines the catch point (if it defines the catchpoint 😮).

We paraphrased Tolstoy in our article on Immutability :

Good designs are all alike; every bad design is bad in its own way.

GOTO (& error management via control flow) considered harmful

It turns out the same principle applies to Exception handling too :-

so many choices! silently swallowing Exceptions — check, throwing Exception — check, catching Exception — check, the list goes on!

Handling Exceptions is not easy, Oracle have dedicated a blog entry to the wrong approaches to Exception handling, that runs to around 15 antipatterns.

We can probably distill those 15 antipatterns into a smaller set of categories and add some new ones of our own — critical Exception related mistakes include

  • Failing to handle error states
  • Failing to anticipate error states
  • Hiding error states (e.g. catch Exception)
  • Complecting control flow

Even the relatively simple task of loading a file from disk can be fraught with danger — in the code below, if we haven’t taken the step of banning null assignments, catching Exception can hide errors caused if file hasn’t been initialized correctly (such as an NPE).

Alternatively if we catch too specific an Exception type, our application flow may jump unexpectedly on error

the compiler doesn’t enforce the handling of RuntimeExceptions

We face a Hobson’s choice in Java of dropping compiler help for managing Exceptions altogether (via RuntimeExceptions) or littering our APIs with throws XXX and attempting limit the specifity of the CheckedExceptions declared (to avoid error hiding). Neither approach is easy to get right in practice.

All of the challenges we face in Java around error handling stem from the fact that we handle Exceptional state by disrupting the flow of control, perhaps after running away in horror from the C++ practice of using error codes (😱😱😱😱)

Photo by Daniel Palma on Unsplash

Testing code that throws Exceptions

Whatever approach we take to managing Exceptions, testing that we’ve got it right is hard. Unit testing methods that throw Exceptions isn’t enough, we need to test the integrations between the methods that may throw Exceptions and their clients to ensure that all handling is correct. Scaling across all combinations of interactions, this isn’t realistically feasible in most applications (see J.B. Rainsberger : Integration Tests are a Scam)

[Functional Concept] Pure Functions : the simplest functions

Pure functions are simple, and this is their big advantage. A pure function always gives the same result for any given input

Function<Integer> addOne = a -> a+1;

Our Function addOne above always give the same answer for each input value.

int two = addOne.apply(1); //always 2
int three = addOne.apply(2); //always 3

This property of pure functions makes them really easy to test, to derive performance benefits from caching, and to reason about in our code. In Java addOne could just as easily be a method and still be a pure function

public int addOne(int a){
return a+1;
} //still a pure function

It is pretty easy to imagine a short but effective suite of unit tests for this method.

APIs that support illegal states are more complicated

On the other hand, if we were to add some logic to addOne that allowed it to throw and Exception, it would no longer be pure

public int addOneImpure(Integer a){
if(a==null)
throw InvalidArgumentException("Argument to addOne can not be null");
return a+1;
}

The key difference to note between the pure addOne and addOneImpure, is that addOneImpure accepts a data type that allows an Exceptional state (it is nullable) to be represented in our code. There are a number of steps we can take to mitigate this

  1. Developer discipline : This is part of the message of Dysfunctional programming in Java 4 : No nulls allowed (🤔). If we ban null assignments and enforce it rigorously with code reviews this exception should never be thrown.
  2. Design our APIs such that our input parameters only reflect permissable states (e.g. in this case by using the non-nullable int type, Java enums, custom case / data classes (roll on Future Java version! — for now roll your own with Lombok / Immutables) are very useful for this too. (This is also part of the message of Dysfunctional programming in Java 4 : No nulls allowed — null fields are often a sign our Object model needs refactoring 🙇🏽‍♀️)
  3. Use the return type to capture that no result could be computed in some manner, perhaps providing feedback on why. (Oh — we covered this too in Dysfunctional programming in Java 4 : No nulls allowed — I’m starting to think the underlying prinicples might be related in some way (🤔🙇🏽‍♀️😳)

Let’s examine Option 2+3 above — and figure out how can we refactor a method that throws an Exception into an pure one that doesn’t.

Converting methods that throw Exceptions to pure form

We can the method addOneImpure pure, and thus easier to test, by removing the exceptional case entirely. That is, we can make the illegal state of an invalid argument unrepresentable in our code. In this case there are two options

  1. Revert the Integer input parameter’s type to the non-nullable int type
  2. Change the return type of the method to a type that captures the optionality of the result when invalid inputs are provided.
good advice

Errors as values

Option 1 (above) is simply the original implementation of addOne while Option 2 involves introducing a new Optional-like return type that is empty when an invalid argument is supplied or present when the inputs are good. Note we don’t recommend you use Optional for this, because, it throws Exceptions (😨😱😨)! Instead we recommend a safe Option type like the one in Cyclops (or if you prefer Functional Java)

Option 2 (above) replaces an exceptional change to control flow with a return value that captures the error state (in this case simply no result).

Better now — no Exceptions!

But what about I/O?

Most of the complexity in typical Java code comes from the fact that we need to interact heavily with external systems (other services, the db, the filesystem, cloud storage etc). Making methods that deal with I/O functionally pure takes a large effort, but taking just the step of not throwing Exceptions from these methods will greatly simplify your code and make it easier to test (even if it remains just a little bit harder to test that addOne — your code should become much easier to test than it is today).

Loading Contents from Disk and from a URL

We’ve already seen an example of this, in our application where rather than throw an Exception when we couldn’t load our Files contents (or download from the Supplied URL) we returned an Option.none instance instead.

The loadContents method uses Option to capture when loading succeeds or fails

If the fact that loadContents failed due to an IOException was significant (or something we wanted to work with) we could capture that in a number of ways

  1. Go style using a Tuple that can contain 2 different types and values
  2. Using an Either type that holds either the correct result or an error result

Using a Tuple type

We could declare loadContents so that it returns a Tuple2. A Tuple2 is a type with two generic type parameters and that holds two values, one of each type. Our loadContents signature would look like this :-

In Go methods that may return errors also return a form of Tuple, in the example below f can be present or nil, err can be present or nil.

Error handling in Go : https://blog.golang.org/error-handling-and-go

After calling loadContents we would have to check if there is an error present before handling, and if there is value present before working with it, hopefully Dysfunctional Programming In Java : No nulls allowed has convinced you this is not a good idea.

⛔️stop⛔️ don’t do this! This is not making illegal states unrepresentable. Instead we need a data type with two type parameters, one for the error and one for the result that holds only one value — either the error or the result.

Using an Either type

A more suitable alternative for this case is a related type : the Either (✅). Like a Tuple2 an Either declares 2 type parameters, unlike a Tuple2 however it only stored one value which may be one of either type specified. It is either the type specified in the left type declarion or the type speficied on the right.

Either<IOException,String> contentsOrError;

Rather than check if contentsOrError has a value of the left type or the right type. Normally we use functional composition to tee up chained operations to handle both cases.

A LazyEither

If we want to capture the IOException while getting the same lazy loading functionality we had originally with Option and Maybe, we’ll need some sort of LazyEither type. Luckily Cyclops provides one. LazyEither is to Either what Maybe is to Option. We can lazily construct an Either by making use of Eval and converting the eager Either returned from loadContents into a LazyEither.

We can refactor the assignment of contents from

import static cyclops.control.Eval.later;Maybe.fromEval(later(this::loadContents))   
.concatMap(Function.identity());

to something like this :-

LazyEither.later(this::loadContents);

And that gives us the same characteristics as we had with Maybe

  1. Is lazily populated on first access ✅
  2. Is only populated once due to Memoization ✅
  3. Does not throw Exceptions if the I/O data is unavailable ✅
  4. Represents the fact the contents data maybe present or absent ✅
  5. 🆕 IOException is captured if one is thrown during loading ✅

The full assignment of contents to an Either looks like this

Let’s unpack this see Use the types Luke : Creating a LazyEither from the types and knowledge

A reactive Either

Similarly we can make use of a CompletableEither, a reactive Either type to replace CompletableMaybe in the loadAsync method.

asynchronously populating a CompletableEither (with Java 10 local type inference — var!)

One difference to note, is that CompletableEither is built on top of the reactive-streams API, which uses Throwable to represent an error. The mapLeft call to this::ioOrThrow casts Throwable to IOException if an error is present and is of the correct type (otherwise an Exception is thrown).

This ioOrThrow implementation looks like this:-

private IOException ioOrThrow(Throwable t){
if(t instanceof IOException){
return (IOException)t;
}
throw ExceptionSoftener.throwSoftenedException(t);
}

Try to catch Exceptions

We are still handling and working with Exceptions imperatively, this is something we can improve with a related data type Try. Try in cyclops is a class with Exception handling methods that wraps or manages an Either type.

We can refactor loadContents to use Try::withCatch which allows us to explicitly define which Exceptions we will catch (avoiding hiding unexpected errors!).

Try has two type parameters!

You can see the method signature of Try in Cyclops (unlike in Scala for example) includes the type of Exception that may be present. Because the error type is present we can convert seamlessly back to an Either with no loss of type information

If you wish to collapse down to a single type parameter and drop the Exception use toOption

And to drop down to a single type parameter but keep the Exception around (if your Try fails), use to(Future::fromTry)

Try with resources

When loading data from a remote URL we also had to open, and close a resource inside the finally block. Try can help us here too

Note the three input parameters to Try::withResources

  1. The BufferedReader to be used and closed
  2. A function that recieves the BufferedReader to use
  3. The Exception type to catch

The complete code with Either and Try looks like this

The key to functional not dysfunctional Exception handling

Is to treat errors as values not as control flow conditions. We listed the categories of problems caused by Exceptional control flow in Java

Photo by Lucas Mordzin on Unsplash
  • Failing to handle error states
  • Failing to anticipate error states
  • Hiding error states (e.g. catch Exception)
  • Complecting control flow

When we switch to using values (via Option, Either or Try) we solve (or at least get compiler help in solving all of these

  • Failing to handle error states [The type system let’s us know an error value is possible, we have to work with the supplied Either / Option / Try] ✅
  • Failing to anticipate error states [The type system let’s us know an error value is possible] ✅
  • Hiding error states (e.g. catch Exception) [The type system can let us know the specific error that is possible — all other errors should result in fatal actual Exceptions that we find and fix as bugs — failing fast and early!] ✅
  • Complecting control flow [No GOTOs or code jumps] ✅

Getting Cyclops

If you are following along, you can install the cyclops library for functional programming in Java by adding it to our Maven our Gradle class paths

Maven

<dependency>
<groupId>com.oath.cyclops</groupId>
<artifactId>cyclops</artifactId>
<version>10.0.4</version>
</dependency>

Gradle

compile group: 'com.oath.cyclops', name: 'cyclops', version: '10.0.4'

--

--

John McClean

Architecture @ Verizon Media. Maintainer of Cyclops. Twitter @johnmcclean_ie