Even in my article about Java exceptions, I didn’t write about how to handle them in lambdas and streams because it deserved its own article.
Exceptions in Lambdas
“Exception handling is a mechanism used to handle disruptive, abnormal conditions to the control flow of our programs.”
Java exception handling evolved over time with additions like multi-catch or try-with-resources, but sadly there aren’t any considerations for lambdas (yet). We still have to oblige to the catch-or-specify requirement of checked exceptions:
I think we can all agree that introducing
catch directly into a lambda is an ugly way to deal with Java’s nonfunctional exception handling.
The throwing and handling of exceptions is the opposite of what we strive to achieve with a more functional coding style. It’s quite verbose and can lead to impurity by making a function no longer deterministic (same input generates the same output). But there are ways to deal with exceptions without losing (most) of the simplicity and clearness of functional programming.
Dealing With Exceptions the Java Way
Exceptions are an essential feature and are here to stay. As much as we might wish for an alternative, we need to find a way to deal with them in our functional code.
There are several ways we can use the normal way in our functional code, with varying degrees of success.
The first way is unchecking our exceptions and thereby eliminating the catch-or-specify requirement. We can do so by creating a wrapper for our calls:
Now, we can wrap an existing function, and an occurring exception will still be thrown — but as an unchecked
The compiler might be happy now, but we didn’t fix the general problem of possible control-flow disruption. And we have no chance of handling it locally at all.
Actually, we didn’t handle any Exceptions — we just hid it from the compiler. Any intricate multistep pipeline we might’ve built with stream operations will collapse entirely if any of them is throwing an exception.
Unchecked exceptions are supposed to be unanticipated and are often unrecoverable. That’s why they don’t fall under the catch-or-specify requirement in the first place! So abusing this concept just to compile our code might not be the best practice we were hoping for.
Even if this kind of handling is acceptable to us, what about
We’d need to create a wrapper for any functional interface that throws an exception.
Handling exceptions by extraction
Instead of just hiding an exception by writing a wrapper, we should actually handle any exception so the control flow might resume in an orderly fashion.
Method references make streams more readable, even without the advantage of the visual removal of exception handling:
We can either handle any
IOException directly in
read(File), or, if it’s not our code to be changed, we could wrap the call in a handling method:
The pipeline is still easy to comprehend, and
safeRead gives us the possibility to handle the exception.
Or, if we can’t, we can still rethrow it as an unchecked exception.
Not throwing exceptions at all
Wrapping exceptions and extracting code to methods is just an abstraction over existing code so we gain control over disruptive conditions.
If we have control over the API, we could design the contract in a way that makes exceptions unnecessary. Or at least more manageable.
Java provides us with
java.util.Optional to represent a nonexisting value that can be handled in a more functional fashion.
We should refrain from returning
null as much as possible to (hopefully) eliminate the dreaded
NullPointerException once and for all. Returning empty collections instead of
null can help, too.
Keep in mind
null isn’t always equivalent to no value found.
null can mean something completely different from returning an
Optional<T>.empty() or an empty collection.
It highly depends on our requirements and how we designed the API contract.
Exceptions are supposed to be additional signals about our control flow. But in a more functional context, we should try to forgo some of them, especially the not-so-obvious ones. It’ll make our code easier to comprehend and reason with.
A More Functional Approach
We have to remember that Java is a general-purpose language, with class-based object-orientation at its core. Even with the addition of lambdas, method references, streams, etc., it didn’t become a full-fledged functional language. But we can look at other, more functional languages to see how to better deal with exceptional conditions.
Scala is also a general-purpose language running on the JVM. It addresses many of Java’s shortcomings and supports functional programming as a first-class citizen.
‘Option’, ‘Some’, ‘None’
But instead of being just a (smart) generic wrapper around another object, it gives you more fine-grained control of the result, built directly into the language itself:
Instead of throwing a possible exception, it was handled directly and replaced by an
Now it can be handled by pattern matching or by any of the methods provided.
‘Try’, ‘Success’, ‘Failure’, and ‘Either’
Scala provides additional types to close this gap:
Option[+A], returns two possible values:
Let’s change our previous example to use these types so they can be more informative:
It’s a great Scala feature, but what does this mean for our Java code?
‘Try’ and ‘Either’ with Java
The most basic functionality of
Try[+T] can be replicated in about 120 lines of code:
Our previous Scala example can now be replicated in Java:
There’s still a lot left to be desired, especially methods for handling and recovering from failures. But you should get the general gist of what’s supposed to be accomplished here.
Instead of implementing all the types and functionality ourselves, we could rely on a well-tried and proven library instead.
There are multiple options available with union types like
Either or improved exception handling like
The Vavr project is aiming to provide all of the tools needed to make Java more functional, like immutability and all the missing functional control structures.
Pattern matching is also supported:
Another option is the Functional Java library.
It doesn’t provide an explicit
Try type, but with its disjoint-union type
Either<A,B>, the same result can be achieved.
It’s not meant to be a complete functional solution like the previous two frameworks.
The best part is its
Seq<T> type, which is like
Stream<T> on steroids.
For handling exceptions, it provides wrappers to easily uncheck checked exceptions:
What’s the best way of dealing with exceptions in our functional Java code? I can’t give you a definitive answer, but hopefully, this article should give you some possibilities.
Which style or library to choose depends highly on how we intend to use and incorporate it into our projects.
A full-functional approach might mean changing the way we design and code our APIs completely. Just unchecking exceptions might not be enough.
Make sure it’s a good fit before committing to a style or library. Before settling on a specific style or library, the best thing is to try multiple approaches with small proof-of-concept projects.
And if you decide to include a third-party library, it’s not an easy decision, and shouldn’t be made lightly. Every dependency has a learning curve, hidden costs, and the possibility of future technical debt.