Error Handling Made Composable With Vavr

A functional approach to exception handling in Java

Alexandre Severo
The Startup
6 min readSep 24, 2020

--

Photo by 傅甬 华 on Unsplash

“Compositionality is the way to control complexity”

— Brian Beckman

In Java, it’s common to throw Exceptions and deal with errors “later” in some try/catch up in the hierarchy. That’s a bad practice. For many reasons, you can be sure about that.

But there’s nothing special about errors. They are values, and as values, they let you program them. Exceptions can crash your program when unhandled, though.

In this article we’re going to talk about the problems of exception handling, what Vavr is and how it helps us to handle errors in a composable way.

Problems With Exception Handling

The usage of exceptions in Java is something that it’s not related to what its name means. Exceptions are not errors, they’re…exceptions.

However, as consequence of a language that does not provide a seamless way to handle errors, eventually we start treating errors as Exceptions, and this leads to a lot of gotos in our code.

Only God knows where they take us.

Suprises

Errors are part of the code, thus they are expected to happen. You shouldn’t ignore them. Instead, errors should be explicit. It makes the code more readable, and it doesn’t catch you with surprises.

Don Corlleone saying: “You come to me at runtime to tell me the code you are executing does not compile?”

Every time you call a function that may raise an exception and don’t catch it on the spot, you create opportunities for surprise bugs caused by functions that terminated abruptly, the data end up in a inconsistent state, or other code paths that you didn’t think about.

Creates Unexpected Exit Points

Beyond the fact that exceptions can bring suprises to your code, it breaks your code into two (or more) different paths. Exceptions are like non-local goto statements. As such they can be used to build general control flow constructs.

This violates the Principle Of Least Astonishment, which states that the result of performing some operation should be obvious, consistent, and predictable. It should be based upon the name of the operation and other clues.

This also makes it harder for programmers to read.

Remember: your code is a story to be read by someone else. The more paths it has, the more complicated it is.

Do you remember this?

Checked Exceptions Doesn’t Go Well With Lambdas

Lambdas can’t throw checked exceptions, only unchecked ones. This happens because the Java Functional Interfaces doesn’t throw anything.

The problem is that lambdas are ubiquitous with Java 8, meaning you have a wide array of hidden errors just waiting for the opportunity to appear.

P.S.: This is why https://github.com/pivovarit/throwing-function or https://projectlombok.org/features/SneakyThrows exist.

They’re not Performant

There’s a big overhead when generating exceptions. When you throw an exception, behind the walls there are a lot of things going on. One of them is the execution of the method below:

public synchronized Throwable fillInStackTrace(){...}

As explained by Heinz Kabutz, in “Cost of Causing Exceptions:”

Fills in the execution stack trace. This method records within this Throwable object information about the current state of the stack frames for the current thread.

Throwing exceptions is one of the most expensive operations in Java, surpassing even new.

It is true there are situations where exceptions are needed, and we have to handle them. Yet, it's easier to get it wrong than to get it right. As a matter of fact, there is a long list of exception patterns you can take a look at. Exceptions are complicated.

Vavr It Is

Vavr, formely called Javaslang, is a object-functional library for Java 8+ that aims to reduce the amount of code we need to write and increase code quality.

It provides persistent collections, functional abstractions for error handling, concurrent programming, pattern matching (which is deprecated in the latest version in favour of Java new pattern matching feature), and much more.

It is heavily built upon values, inspired by Rich Hickey’s (Clojure and Datomic) talk The Value of Values. Therefore, Vavr let us take advantage of values by transforming erros and exceptions in values, for example.

These values have some functional operations such as map, flatMap, filter, fold, etc., which helps us to implement fluent and readable code.

Either

Mathematically, it is a disjoint union. To make things simplier: Either is a monadic structure that represents one of two possible values: either Left or Right.

By default, Left corresponds to a computation that resulted in error; Right corresponds to a computation that resulted in a Success. For this reason, Either is right biased. Still, This behaviour can be changed.

Let's say you need to hypothetically process some user's account and posts saved in a database:

Mother of god…

This is, at best, ugly. But let's see how we would approach using Either:

Better, huh?

map only executes if the returned Either is Right(User), otherwise, processUser will return a Left(Error).

We translated exceptions to errors that can be programmed. In addition to the fact that the code became simpler, now every function is clear about what it may return.

Better than that: every function forces us to handle the return value. With exceptions, we could just declare the exception in the method's name and voilà…yikes!

We can use fold when we need to deal with both left or right values:

Or maybe we want to return different messages in case of error or success:

fold() to the rescue!

These examples are quite simple. Every function was implemented by us. This way we can get rid of the exceptions and return Eithers. but what if we're dealing with an external library, which we're obligated to handle?

Try

The definition from Vavr’s user guide says that:

Try is a monadic container type which represents a computation that may either result in an exception, or return a successfully computed value. It’s similar to, but semantically different from Either. Instances of Try, are either an instance of Success or Failure.

The difference between Either and Try is that with Try we can handle Exceptions. This is great when we need to execute some computation that we don't have control over, and it may throw an exception.

Let's assume we want to process an objects with an external library:

The problem here is that SomeLibrary::process throws a checked exception. We all know that lambdas doesn't support checked exceptions. In this case, we can handle that using Try (and Either):

Or, if we don't need to return anything:

void return types means one thing: side effects!

Something to note: did you see how using Try and Either to transform exceptions in error values allowed us to use functions as values (and how methods with side effects cannot be treated as values)?

Both Either and Try have a lot of other functions to help you handle errors and control data flow seamlessly with their fluent APIs. The list is long and the applications are endless.

Conclusion

The use of exceptions to handle errors is one of the big mistakes in Java development, hence it should be avoided. Error are values and you ought to treat them as so.

Programming errors gives a lot of advantages, such as the ability to control data flow, write readable code and avoid GOTOs and surprises with exceptions. Vavr is a library that helps us with that.

That's all folks! I see you in the next article!

References

--

--