In my previous articles about functional programming, I’ve shown how to incorporate a more functional style into our Java code. But I’ve omitted a crucial topic: how to deal with exceptions.

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.


Pure Functions

One of the base concepts of functional programming is that of pure functions. This type of functions generate the same output for the same input without any side effects — like affecting global state. But as stated in a previous article:

“Exception handling is a mechanism used to handle disruptive, abnormal conditions to the control flow of our programs.”

Even with lambdas, we have to oblige to the catch-or-specify requirement of checked exceptions:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
String read(File input) throws IOException {
    // ...
}

Stream<File> files = ...

// ERROR: Unhandled Exception of type IOException
files.map(this::read)
     .filter(Objects::nonNull)
     .forEach(System.out::println);
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
files.map(file -> {
         try {
             return read(file);
         }
         catch (IOException e) {
             // handle the exception...
             return null;
         }
     })
     .filter(Objects::nonNull)
     .forEach(System.out::println);

I think we can all agree that introducing try-catch directly into a lambda is an ugly way to deal with Java’s nonfunctional exception handling.

Exceptions are the opposite of what we strive to achieve with a functional coding style. But there are ways to handle them without losing (most) of the 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.

Unchecking exceptions

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@FunctionalInterface
public interface ExFunction<T, R, E extends Exception> {
    R apply(T t) throws Exception;
}

<T, R> Function<T, R> uncheck(ExFunction<T, R> fn) {
    return t -> {
        try {
            return fn.apply(t);
        }
        catch (Exception e) {
            throw new RuntimeException(e);
        }
    };
}

Now, we can wrap an existing function, and an occurring exception will still be thrown — but as an unchecked RuntimeException instead:

1
2
3
4
// Compiler is happy
files.map(uncheck(this::read))
     .filter(Objects::nonNull)
     .forEach(System.out::println);

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 exception — 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 BiFunction<T,U,R>, Consumer<T>, Supplier<T>, etc.? 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:

1
2
3
4
5
6
7
8
9
files.map(uncheck(read(file -> read(file)))
     .filter(content -> Objects.nonNull(content))
     .forEach(content -> System.out.println(content));

// - VS -

files.map(uncheck(this::read))
     .filter(Objects::nonNull)
     .forEach(System.out::println);

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
String safeRead(File file) {
    try {
        String content = read(file);
    } catch (IOException e) {
        // ...
        return null;
    }
}

files.map(this::safeRead)
     .filter(Objects::nonNull)
     .forEach(System.out::println);

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.

Returning 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’

The Option[+A] type is Scala’s way of dealing with nullable values, just like java.util.Optional<T>.

But instead of being just a (smart) generic wrapper around another object, it gives you a more fine-grained control of the result, built directly into the language itself:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def toInt(str: String): Option[Int] = {
    try {
        Some(Integer.parseInt(str.trim))
    } catch {
        case e: Exception => None
    }
}

toInt("1") match {
    case Some(result) => println(result)
    case None => println("Can't convert")
}

Instead of throwing a possible exception, it was handled directly and replaced by an Option[Int]. Now it can be handled by pattern matching or by any of the methods provided.

Option[+A] is way more powerful than java.util.Optional<T> But it still lacks the additional information provided by an exception: what went wrong.

‘Try’, ‘Success’, ‘Failure’, and ‘Either’

Scala provides additional types to close this gap:

Try[+T], like Option[+A], returns two possible values:

Let’s change our previous example to use these types so they can be more informative:

1
2
3
4
5
val result = Try(Integer.parseInt("123".trim))
result match {
    case Success(v) => println(result)
    case Failure(e) => println("Exception occured: " + e.getMessage)
}

Besides Try[+T], a more general Either[+A, +B] is also available, which isn’t bound to Throwable.

It’s a great Scala feature, but what does this mean for our Java code?

‘Try’ and ‘Either’ with Java

The general concept behind Try[+T] and Either[+A, +B] can be applied in Java as well, although not as nicely done as in Scala.

The most basic functionality of Try[+T] can be replicated in about 120 lines of code:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
import java.util.Objects;
import java.util.function.Function;
import java.util.function.Supplier;

public abstract class Try<T> {

    private final boolean success;

    public Try(boolean success) {
        this.success = success;
    }

    public boolean isSuccess() {
        return this.success;
    }

    public boolean isFailure() {
        return this.success == false;
    }

    public abstract Throwable getThrownMessage();

    public abstract T get();

    public abstract <U> Try<U> map(Function<? super T, ? extends U> fn);

    public abstract <U> Try<U> flatMap(Function<? super T, Try<U>> fn);

    static <T> Try<T> failure(Throwable t) {
        Objects.requireNonNull(t);
        return new Failure<>(t);
    }

    static <V> Try.Success<V> success(V value) {
        Objects.requireNonNull(value);
        return new Success<>(value);
    }

    static <T> Try<T> of(Supplier<T> fn) {
        Objects.requireNonNull(fn);
        try {
            return Try.success(fn.get());
        }
        catch (Throwable t) {
            return Try.failure(t);
        }
    }

    static class Failure<T> extends Try<T> {

        private final RuntimeException exception;

        public Failure(Throwable t) {
            super(false);
            this.exception = new RuntimeException(t);
        }

        @Override
        public T get() {
            throw this.exception;
        }

        @Override
        public <U> Try<U> map(Function<? super T, ? extends U> fn) {
            Objects.requireNonNull(fn);
            return Try.failure(this.exception);
        }

        @Override
        public <U> Try<U> flatMap(Function<? super T, Try<U>> fn) {
            Objects.requireNonNull(fn);
            return Try.failure(this.exception);
        }

        @Override
        public Throwable getThrownMessage() {
            return this.exception;
        }
    }

    static class Success<T> extends Try<T> {

        private final T value;

        public Success(T value) {
            super(true);
            this.value = value;
        }

        @Override
        public T get() {
            return this.value;
        }

        @Override
        public <U> Try<U> map(Function<? super T, ? extends U> fn) {
            Objects.requireNonNull(fn);
            try {
                return Try.success(fn.apply(this.value));
            }
            catch (Throwable t) {
                return Try.failure(t);
            }
        }

        @Override
        public <U> Try<U> flatMap(Function<? super T, Try<U>> fn) {
            Objects.requireNonNull(fn);
            try {
                return fn.apply(this.value);
            }
            catch (Throwable t) {
                return Try.failure(t);
            }
        }

        @Override
        public Throwable getThrownMessage() {
            throw new IllegalStateException("Success never has an exception");
        }
    }
}

Our previous Scala example can now be replicated in Java:

1
2
3
4
5
6
Try<Integer> result = Try.ofFailable(() -> Integer.parseInt(strValue);
if (result.isSuccess) {
    ...
} else {
    System.out.println("Exception occured: " + result.getThrownException().getMessage());
}

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.

By using a disjoint-union type, we gain the possibility of representing multiple states in a single object. Other languages use tuple-based patterns, like Golang error handling, to achieve the same.

But a dedicated type can be extended with methods, like map, flatMap, fold, etc., to better fit into a functional coding-style.


Third-Party Libraries

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 Try.

Vavr

&ldquo;Vavr project logo
https://www.vavr.io/

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// OPTION
Option<String> maybe = Option.of("a value");
String value = maybe.peek(System.out::println)
                    .map(String::toUpperCase)
                    .orElse("n/a");

// TRY
Try.of(() -> mightThrow())
   .map(String::toUpperCase)
   .onSuccess(System.out::println)
   .onFailure(this::handleThrowable)
   .andFinally(this::cleanup);

Try.run(() -> mightightThrowNoReturnValue())
   ...

Try.withResouces(() -> new BufferedInputStream(...))
   ...

Pattern matching is also supported:

1
2
3
4
5
6
Try<String> _try = Try.of(() -> mightThrow());

Match(_try).of(
    Case($Success($()), value -> ...),
    Case($Failure($()), x -> ...)
);

Functional Java

&ldquo;Functional Java project logo
https://www.functionaljava.org/

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.

1
2
3
4
5
6
7
8
public Either toInt(String input) {
    try {
        return Either.right(Integer.valueOf(input));
    }
    catch (Exception e) {
        return Either.left(Fail.invalidInteger(input));
    }
}

jOOλ

https://github.com/jOOQ/jOOL

The creators of jOOQ, a great SQL library, also created jOOλ, which provides some of the missing parts of Java’s lambdas.

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Arrays.stream(dir.listFiles()).forEach(file -> {
    try {
        System.out.println(file.getCanonicalPath());
    }
    catch (IOException e) {
        throw new RuntimeException(e);
    }
});

// - VS -

Arrays.stream(dir.listFiles())
      .map(Unchecked.function(File::getCanonicalPath))
      .forEach(System.out::println);

Conclusion

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.


Resources