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();
// 42So 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, anywayIt 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(); // 42Conclusion
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.



