Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why Java 8 CompletableFuture thenCompose generates different exception depending on the order of completion?

I have encountered strange behavior of Java 8 CompletableFuture thenCompose method. I have two tests that differ only in order of execution. Both tests simulate failure in the CompletableFuture generated in thenCompose.

@Test
public void completedAfter() {
    CompletableFuture<String> future1 = new CompletableFuture<>();
    CompletableFuture<String> future2 = new CompletableFuture<>();

    future1.thenCompose(x -> future2).whenComplete((r, e) -> System.out.println("After: " + e));

    future1.complete("value");
    future2.completeExceptionally(new RuntimeException());
}

@Test
public void completedBefore() {
    CompletableFuture<String> future1 = new CompletableFuture<>();
    CompletableFuture<String> future2 = new CompletableFuture<>();

    future1.complete("value");
    future2.completeExceptionally(new RuntimeException());

    future1.thenCompose(x -> future2).whenComplete((r, e) -> System.out.println("Before: " +e));
}

The output is:

After: java.util.concurrent.CompletionException: java.lang.RuntimeException
Before: java.lang.RuntimeException

The question is, why is the exception wrapped in CompletionException in one case but not in the other?

Update: Here is related bug report. It has been marked and resolved as a bug in JDK.

like image 921
Lukas Avatar asked Jan 02 '15 20:01

Lukas


1 Answers

Seems like a bug in the jdk library.

In the "After" case, .thenCompose adds a ThenCopy completion node to the target future, whose execution is later triggered by .completeExceptionally. The completion node's run method finds the exception on the future, and calls .internalComplete on the destination, that wraps all exceptions into CompletionException. See here how the node is created, and here for where the wrapping happens.

Now, in the Before case, the code path is completely different. Because the future is already completed, .thenCompose does not create additional nodes, but invokes the callback right away, and simply returns an (already completed second future), on which you then call .whenComplete, which, again, does not bother to create a new completion node, but simply invokes the callback right away, giving it the original exception from the second future.

Boy, looking at this code, there are so many examples I want to show to my students of what they should never do ...

like image 139
Dima Avatar answered Nov 15 '22 01:11

Dima