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.