Press "Enter" to skip to content

Be Careful with CompletableFuture.applyToEither and Exceptions

In this article, we’ll revisit the CompletableFuture.applyToEither method and try to figure out a workaround for one of its issues.

CompletableFuture.applyToEither and its Quirks

The CompletableFuture.applyToEither method is pretty self-explanatory. The idea is that you can declaratively provide a function that should be applied to a value of the first CompletableFuture that completed normally.

Imagine that we have two futures and want to print the value of whichever comes first:

CompletableFuture<Integer> f1 = CompletableFuture.completedFuture(42);
CompletableFuture<Integer> f2 = new CompletableFuture<>();

f1.applyToEither(f2, i -> i).thenAccept(System.out::println).join();

// 42

Naturally, if we swap f1 with f2, we expect to witness the same result:

CompletableFuture<Integer> f1 = CompletableFuture.completedFuture(42);
CompletableFuture<Integer> f2 = new CompletableFuture<>();

f2.applyToEither(f1, i -> i).thenAccept(System.out::println).join();

// 42

Which is indeed the case.

So, where’s the problem?

Exception Handling

Unfortunately, the above breaks apart if we sprinkle some exceptions on them.

CompletableFuture<Integer> f1 = CompletableFuture.completedFuture(42);
CompletableFuture<Integer> f2 = CompletableFuture.failedFuture(new NullPointerException("oh no, anyway"));

f1.applyToEither(f2, i -> i).thenAccept(System.out::println).join();

// 42

So far, so good, but what happens if we swap f1 with f2 again?

CompletableFuture<Integer> f1 = CompletableFuture.completedFuture(42);
CompletableFuture<Integer> f2 = CompletableFuture.failedFuture(new NullPointerException("oh no, anyway"));

f2.applyToEither(f1, i -> i).thenAccept(System.out::println).join();

// Exception in thread "main" java.util.concurrent.CompletionException: java.lang.NullPointerException: oh no, anyway

It turns out that despite the fact that the other future is already completed, we never progress because the exception ends up propagating to the joint future which is not a behaviour many would expect.

Personally, I perceive it as a bug.

Solution

In order to circumvent the issue, we need to craft a new method since CompletableFuture.anyOf behaves in a similar fashion and won’t be helpful here.

To do that, we need to simply create a new CompletableFuture and introduce a race between two completions:

public static <T> CompletableFuture<T> either(
  CompletableFuture<T> f1, CompletableFuture<T> f2) {
    CompletableFuture<T> result = new CompletableFuture<>();
    // ...

    f1.thenAccept(result::complete);
    f2.thenAccept(result::complete);
    return result;
}

However, it’s not enough. What if all futures completed exceptionally? We’d be stuck with an incomplete future forever.

This can be achieved by piggybacking onto CompletableFuture.allOf:

CompletableFuture.allOf(f1, f2).whenComplete((__, throwable) -> {
    if (f1.isCompletedExceptionally() && f2.isCompletedExceptionally()) {
        result.completeExceptionally(throwable);
    }
});

And here is the complete sample:

public static <T> CompletableFuture<T> either(
  CompletableFuture<T> f1, CompletableFuture<T> f2) {
    CompletableFuture<T> result = new CompletableFuture<>();
    CompletableFuture.allOf(f1, f2).whenComplete((__, throwable) -> {
        if (f1.isCompletedExceptionally() && f2.isCompletedExceptionally()) {
            result.completeExceptionally(throwable);
        }
    });

    f1.thenAccept(result::complete);
    f2.thenAccept(result::complete);
    return result;
}

And in action:

CompletableFuture<Integer> f1 = CompletableFuture.completedFuture(42);
CompletableFuture<Integer> f2 = CompletableFuture.failedFuture(new NullPointerException("oh no, anyway"));

either(f1, f2).thenAccept(System.out::println).join(); // 42
either(f2, f1).thenAccept(System.out::println).join(); // 42

Conclusion

CompletableFuture.applyToEither works in a manner that’s mostly unacceptable for production and if you are looking after similar behaviour, you might need to craft a suitable utility method yourself.

All code samples can be found on GitHub along with other CompletableFuture utilities.




If you enjoyed the content, consider supporting the site: