Dealing with Java Exceptions in a functional way

Przemysław Sobierajski

You may have heard that checked exceptions in Java are evil. Some people even say that they are Java’s biggest mistake. There is a lot of languages like Scala, Kotlin, C# or C++ which don’t have checked exceptions at all. Unchecked exceptions are generally better choice. Undoubtedly, you are able to write your code without creating new checked exceptions. However, you have to deal with them constantly, because a lot of standard or popular libraries abuse them. In result, your Java code is full of ugly throw catch clauses. They interfere with a regular application control flow.

I’m not going to consider negative aspects of using checked exceptions, because it’s a topic for the next article. I’d like to show you how to get rid of them completely from your Java code. To do this, you need to use some monadic containers from Vavr.io library. You can add the dependency to your pom.xml file.

<dependency>
    <groupId>io.vavr</groupId>
    <artifactId>vavr</artifactId>
    <version>0.9.2</version>
</dependency>

Practical example

Let’s say you want to create a method which execute the given tasks, returning the result of one that has completed successfully (i.e., without throwing an exception):

private <T> T invokeAny(Collection<? extends Callable<T>> tasks,
                        long timeout, TimeUnit timeUnit) {
    ExecutorService executor = Executors.newFixedThreadPool(
            tasks.size() % Runtime.getRuntime().availableProcessors());
    return executor.invokeAny(tasks, timeout, timeUnit);
}

Looks good, but unfortunately it won’t work. The compiler says: “Unhandled exceptions: java.lang.InterruptedException, java.util.concurrent.ExecutionException, java.util.concurrent.TimeoutException”.

The easiest way to fix it is to add catch clauses and possibly return Optional instead of valid result or null.

private <T> Optional<T> invokeAny(Collection<? extends Callable<T>> tasks,
                                  long timeout, TimeUnit timeUnit) {
    ExecutorService executor = Executors.newFixedThreadPool(
            tasks.size() % Runtime.getRuntime().availableProcessors());
    try {
        return Optional.ofNullable(executor.invokeAny(
                tasks, timeout, timeUnit));
    } catch (InterruptedException e) {
        dealWith(e);
    } catch (ExecutionException e) {
        dealWith(e);
    } catch (TimeoutException e) {
        dealWith(e);
    }
    return Optional.empty();
}

private void dealWith(ExecutionException exc) {
    //some logging or other stuff
}

private void dealWith(InterruptedException exc) {
    //some logging or other stuff
}

private void dealWith(TimeoutException exc) {
    //some logging or other stuff
}

Now it looks very bad, but it compiles at least. Let’s introduce some unit tests which will assure you that code works for both cases: happy path and some exception was thrown.

@Test
void invokeAnyHappyPathTest() {
    Callable<Integer> task1 = () -> 1;
    Callable<Integer> task2 = () -> 2;

    Optional<Integer> result = invokeAny(asList(task1, task2),
            1, TimeUnit.SECONDS);

    assertThat(result.isPresent()).isTrue();
    assertThat(result.get()).isIn(1, 2);
}

@Test
void invokeAnyExceptionPathTest() {
    Callable<Integer> task1 = this::sleepFor200Ms;
    Callable<Integer> task2 = this::throwInterruptedException;

    Optional<Integer> result = invokeAny(asList(task1, task2),
            100, TimeUnit.MILLISECONDS);

    assertThat(result.isPresent()).isFalse();
}

private Integer sleepFor200Ms() throws InterruptedException {
    Thread.sleep(200);
    return 1;
}

private Integer throwInterruptedException() throws InterruptedException {
    throw new InterruptedException();
}

Using Try

Let’s get rid of catch clauses from invokeAny() methods. Of course, we can add throws <list of exceptions> to the function definition, but it will only pass the problem to the method clients.

I have better idea. Let’s use Try monadic container from Vavr library.

private <T> Try<T> invokeAny(Collection<? extends Callable<T>> tasks,
                                  long timeout, TimeUnit timeUnit) {
    ExecutorService executor = Executors.newFixedThreadPool(
            tasks.size() % Runtime.getRuntime().availableProcessors());
    return Try.of(() -> executor.invokeAny(tasks, timeout, timeUnit))
            .onFailure(this::dealWithThrowable);
}

private void dealWithThrowable(Throwable throwable) {
    //some logging or other stuff
}

You need to change the result type and assertions in your unit tests:

@Test
void invokeAnyHappyPathTest() {
    Callable<Integer> task1 = () -> 1;
    Callable<Integer> task2 = () -> 2;

    Try<Integer> result = invokeAny(asList(task1, task2),
            1, TimeUnit.SECONDS);

    result.onSuccess(i -> assertThat(i).isIn(1, 2));
    result.onFailure(Assertions::fail);
}

@Test
void invokeAnyExceptionPathTest() {
    Callable<Integer> task1 = this::sleepFor200Ms;
    Callable<Integer> task2 = this::throwInterruptedException;

    Try<Integer> result = invokeAny(asList(task1, task2),
            100, TimeUnit.MILLISECONDS);

    result.onSuccess(i -> Assertions.fail(
            "expected exception, but there was a result: " + i));
    result.onFailure(thr -> assertThat(thr).isInstanceOfAny(
            InterruptedException.class, TimeoutException.class));
}

What has happened in above example? ExecutorService::invokeAny method has been called inside of the Try container. Try deals with catching checked exception on your behalf. If you would like to recover exception which has been thrown and deal with each of them in different way, you could use recoverWith method:

return Try.of(() -> executor.invokeAny(tasks, timeout, timeUnit))
        .recoverWith(ExecutionException.class, this::dealWith)
        .recoverWith(InterruptedException.class, this::dealWith)
        .recoverWith(TimeoutException.class, this::dealWith);

Please note that dealWith method for each specific exceptions should be changed to:

private <T> Try<T> dealWith(ExecutionException exc) {
    //some logging or other stuff
    return Try.failure(exc);
}

Unit tests still pass after such change.

There is a lot of possible actions you can do with Try instance. It has rich API. I encourage you to take a look at documentation (https://static.javadoc.io/io.vavr/vavr/0.9.2/io/vavr/control/Try.html) and try some actions on your own.

Using Either

Another option to consider is to use Either instead of Try. It represents a value of two possible types: either a Left or a Right. Right is considered as success case and Left is failure. Let’s see an example:

private <T> Either<RuntimeException, T> invokeAny(
        Collection<? extends Callable<T>> tasks, long timeout,
        TimeUnit timeUnit) {

    ExecutorService executor = Executors.newFixedThreadPool(
            tasks.size() % Runtime.getRuntime().availableProcessors());
    return Try.of(() -> executor.invokeAny(tasks, timeout, timeUnit))
            .onFailure(this::dealWithThrowable)
            .toEither(RuntimeException::new);
}

After change, the method returns Either<RuntimeException, T>. It means that the methods will return either a valid result (T) or an instance of RuntimeException in case of failure.

Again, result type and assertions has changed a little bit:

Test
void invokeAnyHappyPathTest() {
    Callable<Integer> task1 = () -> 1;
    Callable<Integer> task2 = () -> 2;

    Either<RuntimeException, Integer> result = invokeAny(
            asList(task1, task2), 1, TimeUnit.SECONDS);

    result.right().peek(i -> assertThat(i).isIn(1, 2));
    result.peekLeft(Assertions::fail);
}

@Test
void invokeAnyExceptionPathTest() {
    Callable<Integer> task1 = this::sleepFor200Ms;
    Callable<Integer> task2 = this::throwInterruptedException;

    Either<RuntimeException, Integer> result = invokeAny(
            asList(task1, task2), 100, TimeUnit.MILLISECONDS);

    result.right().peek(i -> Assertions.fail(
            "expected exception, but there was a result: " + i));
    result.peekLeft(thr -> assertThat(thr)
            .isInstanceOf(RuntimeException.class));
}

How do these assertions work? It’s simple. There is a result which is an instance of Either<RuntimeException, Integer>Right() method gets the right projection of the result (which is an Integer) and peek() method will perform some action (in this case – assertion) only if the Either is a Right instance (the value has been successfully returned from invokeAny() method).

PeekLeft() method works analogously. It will consume Throwable instance from Left projection, if it’s present, and perform an action.

The alternative path

Either doesn’t have to be used in success or failure pattern. It can also be used to returning default or alternative path as a result. You could change a definition of invokeAny() method from:

private <T> Either<RuntimeException, T> invokeAny(. . .)

to:

private <D, T> Either<D, T> invokeAny(. . .)

where D is a default result, which can occur in case of a failure. As you can see, it can also have another type than returned value.

Summary

Using checked exceptions in Java to change application control flow is considered as a bad practice. Fortunately, you can get rid of it by using monads and functional programming concept. Vavr.io library provides a set of solutions to write Java code in more functional way. Monadic containers like Try and Either can be used to deal with checked exceptions. Besides that, they have very rich API which make them worth to try in everyday use.

Poznaj mageek of j‑labs i daj się zadziwić, jak może wyglądać praca z j‑People!

Skontaktuj się z nami