Why is the result of this code non-deterministic?

Viewed 111

I would expect the following code to always print "Errored", but sometimes it prints "outerinner". What I want to happen is for "outer" to complete and for "inner" to use "outer"'s result to generate its own result, failing if either future fails. What am I doing wrong?

        CompletableFuture<String> outer = CompletableFuture.supplyAsync(() -> "outer");
        CompletableFuture<String> inner = CompletableFuture.supplyAsync(() -> "inner");
        inner.completeExceptionally(new IllegalArgumentException());
        CompletableFuture<String> both = outer.thenApply(s -> {
            try {
                String i = inner.get();
                return s + i;
            } catch (InterruptedException |ExecutionException e) {
                throw new IllegalStateException(e);
            }
        });

        try {
            String o = both.get();
            System.out.println(o);
        } catch (ExecutionException | InterruptedException e) {
            System.err.println("Errored");
        }
2 Answers

It's actually fairly trivial to understand this one. You need to look more closely at this:

CompletableFuture<String> inner = CompletableFuture.supplyAsync(() -> "inner");

specifically the fact that you are saying supplyAsync, which means in a different thread. While in your main thread (or any other) you do : inner.completeExceptionally(new IllegalArgumentException());.

Which thread is supposed to win? The one from supplyAsync or the one where you run inner.completeExceptionally? Of course, the documentation of completeExceptionally mentions this... It's a basic "race", which thread reaches to "complete" (either normally or via an Exception) that inner first.

The key to understanding what is going on is in the javadoc for completeExceptionally.

If not already completed, causes invocations of get() and related methods to throw the given exception.

In your example, you are creating two futures which will be completed asynchronously, then you are calling completeExceptionally to tell one of the futures to throw an exception rather than delivering a result.

Based on what you are saying, it appears that sometimes the inner future has already completed by the time you call completeExceptionally. In that case, the completeExceptionally call will have no affect (as per the spec), and the subsequent inner.get() will deliver the result that was (already) computed.

It is a race condition.

Related