Exception handling in Functional Programming

Java Jedi
6 min readNov 2, 2023

With the advent of lambda expressions, i.e. functional interfaces, in Java, it has become a joy using functional interfaces and streams especially when working with lists or other collection and there is a need to manipulate data. We like the way how lambdas make other code clean, concise, brief and elegant. The common methods used with streams are: map, filter, reduce, forEach, etc.
But the problem begins when we need to deal with exception handling.

Let’s consider the following example:

We have a UserService with a single method — findUser() that accepts an id and returns a User, also this method throws a checked exception.

Then in the main method, we have a list of ids and want to retrieve Users by ids. In line 7, we transform ids into a stream and for each id in a stream we call findUser() function to retrieve a User. Then, in line 8 we print each user.

However, this code never compiles. Why?
The answer is simple — findUser() throws checked exception — InterruptedException. If a method throws a checked exception, we have to handle the exception while calling this method.
Ok🤔, this is not a problem, we can wrap the function call inside try-catch block.

Actually, this approach works and the program can be executed but wrapping the function call into try-catch inside a lambda is not an appropriate solution, because it makes our code less readable and clean, our code loses its brevity and conciseness. We love streams for their brevity and conciseness but in this example we losing all these qualities.

There are several ways to handle this issue and next we will talk about these approaches.

Extract try-catch block into a separate method

This is a common approach that I have seen many times. Well, this approach is clean and concise in terms of code readability. Besides, this approach is taken further — generalizing extract method.

Generalizing extract method

This is a generic way to handle checked exception in streams. We declared a new functional interface FunctionEx with a method that throws a checked exception. Additionally, we declared a new method execute() that accepts a FunctionEx interface as its argument and returns a java.util.function.Function. Method execute() just invokes the function that receives as its argument and wraps the function inside try-catch block.
This approach enables us to pass any function to execute() function as an argument no matter what kind of exception it throws.
As you can see, our code is elegant and concise.

Passing findUser() to execute() method

Let’s take a look at the way we are dealing with the exception — if there is an exception, we are just re-throwing the checked exception by wrapping into unchecked exception. And here we need to ask ourselves — Did we really handle the exception? Well, it depends.

Re-throwing exception
Output

By re-throwing the exception, we face the following situation shown on the picture. We are processing the stream and have already processed several elements but when exception happens, we lose all the processed elements and the remaining elements of the stream are never processed.

Stream::map illustration

There are two possible outcomes:
- if there is am exception and it is important that the whole stream must be processed atomically, then we stop executing and re-throwing exception is an appropriate approach. We don’t care about the previously processed elements and rollback.
- if there is an exception and it is important to process the whole stream till the end, then re-throwing the exception is not an appropriate approach. We need to continue processing the stream till the end. Next we will talk about how to deal with this problem.

Dealing with exceptions as data

Another concept of dealing with exceptions that I favour the most came from functional programming languages, such as Haskell and Scala. The idea is simple yet efficient. This concept says — what if we treat exceptions as data? To implement this idea, we need to construct the following hierarchy.

Try class hierarchy

Try — is a sealed class that permits only two classes to extend — Success and Failure:
- class Success holds a single generic value that is the result of some computation.
- class Failure holds a single value of type Exception that displays the reason of a computation failure.

Next, we need a function that accepts a Supplier interface as its argument and has a return type Try. By calling this method we get a Try instance as a result of computation and the result can be cast either to a Success object with the result of computation or a Failure object with the reason of computation failure. Let’s implement all these concepts.

We declare a sealed abstract class Try<T> that permits Success and Failure to extend. Additionally, it has a static method apply() — that accepts a function — Supplier interface as its argument and returns either Success or Failure depending on the result of supplier function execution.

Next we define Success and Failure classes.

A Success.class with a single final generic value — the result of computation

A Failure.class with a single final Exception value — the exception that caused the failure of computation.

Let’s put all together and see how it works

As you can see, our code is purely functional and the result of every computation is a Try object. We pass our findUser() method to Try.apply() function and get a Try instance that can be safely cast to either Success or Failure. Then, we can proceed with any logic we want. We are not re-throwing the exception anymore and treating the exception as data. The code looks concise and clean.

Output

Previously, we encountered a problem with processing a stream that is broken when an exception occurs. However, we want to continue stream processing and handle the exception later. With this approach, this is no longer an issue. We can freely process the stream and manipulate the data.

Output

As you can see from the output, we achieved to process the stream till the end and print the users retrieved as well as exception message.

By applying this approach, when we process a stream, we get a stream of Try objects that can be safely cast to Success or Failure objects.

However, it has its own disadvantage as well. This approach brings extra layer over our data model. In order to apply some logic, we need to check whether it is failure or success (look at the snipped above).

In conclusion, in this post we have discussed several ways of handling exceptions in streams and how to implement our own functional approach of dealing with exceptions.

--

--