JDK-8068432 : Inconsistent exception handling in CompletableFuture.thenCompose
  • Type: Bug
  • Component: core-libs
  • Sub-Component: java.util.concurrent
  • Affected Version: 8,8u25,9
  • Priority: P4
  • Status: Closed
  • Resolution: Fixed
  • Submitted: 2015-01-03
  • Updated: 2016-08-24
  • Resolved: 2015-01-21
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
JDK 8 JDK 9
8u60Fixed 9 b48Fixed
Description
FULL PRODUCT VERSION :
java version "1.8.0_25"
Java(TM) SE Runtime Environment (build 1.8.0_25-b17)
Java HotSpot(TM) 64-Bit Server VM (build 25.25-b02, mixed mode)


ADDITIONAL OS VERSION INFORMATION :
Linux  3.13.0-37-generic #64-Ubuntu SMP Mon Sep 22 21:28:38 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux

A DESCRIPTION OF THE PROBLEM :
CompletableFuture.thenCompose inconsistently handles exception thrown from the CompletionStage created by function. See the steps to reproduce 

STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
I have this two tests that only differ in order of execution but produce different output.

@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));
}

EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
Consistent output, most likely:

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

ACTUAL -
Inconsistent output:

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


REPRODUCIBILITY :
This bug can be reproduced always.

---------- BEGIN SOURCE ----------
import org.junit.Test;

import java.util.concurrent.CompletableFuture;

public class CompletableFutureComposeTest {

    @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));
    }
}
---------- END SOURCE ----------


Comments
the spec for thenCompose is not sufficiently clear. There are 3 CompletionStages in play and there are 4 ways that exceptional completion can happen and this is not obvious. - source could fail - action could fail - result of action could fail - user could exceptionally complete the terminal future explicitly The scenario where users complete the terminal future is undertested.
07-01-2015

We had tck tests for thenCompose that check failing source, or failing function, but not failing _result_ of the function. I committed a new tck test for that in jsr166 CVS CompletableFutureTest that passes with latest code: /** * thenCompose result completes exceptionally if the result of the action does */ public void testThenCompose_actionReturnsFailingFuture() { (I also find thenCompose to be confusing)
07-01-2015

Oops, the above diff didn't handle uncompleted case. I committed update (not pasted here to reduce confusion).
06-01-2015

Thanks, now I see the issue: Before the introduction of CompletionStage, CompletableFuture.thenCompose spec said * Returns a CompletableFuture (or an equivalent one) produced by * the given function of the result of this CompletableFuture when * completed.... Which allows current behavior. But the CompletionStage version lost the disclaimer, and says * Returns a new CompletionStage ... Which now obligates CF to create a new stage in all cases (because it cannot guarantee that the one produced by the function is new). Which adds only a little more time/space. Committed into our CVS after passing all tests: *** CompletableFuture.java.~1.138.~ 2015-01-05 07:13:13.114001843 -0500 --- CompletableFuture.java 2015-01-06 10:51:04.711438741 -0500 *************** *** 939,945 **** } try { @SuppressWarnings("unchecked") T t = (T) r; ! return f.apply(t).toCompletableFuture(); } catch (Throwable ex) { return new CompletableFuture<V>(encodeThrowable(ex)); } --- 939,946 ---- } try { @SuppressWarnings("unchecked") T t = (T) r; ! return new CompletableFuture<V>( ! encodeRelay(f.apply(t).toCompletableFuture().result)); } catch (Throwable ex) { return new CompletableFuture<V>(encodeThrowable(ex)); }
06-01-2015

I do find implementation somewhat confusing especially when an "fe.join()" throws CompletionException (there is obviously no other option in that case) and I presume in nearly all cases the exception reported via whenComplete or handle will be a CompletionException. In hindsight perhaps we should have specified that the exceptions reported via whenComplete or handle are always reported unwrapped from any CompletionException? However, I would argue there is still an implementation issue. Going back to the original case with thenCompose: CompletableFuture<String> f = new CompletableFuture<>(); CompletableFuture<String> fe = new CompletableFuture<>(); f.complete(""); fe.completeExceptionally(new RuntimeException()); CompletableFuture<String> f_thenCompose = f.thenCompose(s -> fe); f_thenCompose.whenComplete( (r, e) -> System.out.println("f_thenCompose.whenComplete: " + e)); The "f_thenCompose" is a dependent stage (dependent on "f" and then "fe"). However, depending on the program order of "f" completing, "f_thenCompose" will complete exceptionally with RuntimeException or it being wrapped in CompletionException. Change "thenCompose" to "thenComposeAsync" and then "f_thenCompose" always completes with CompletionException. There is a quite reasonable optimization for "thenCompose" in CF.uniComposeStage that if "f" is successfully complete then the dependent stage is "fe" itself. But that optimization is cause of the inconsistency described above. Should we remove that optimization?
06-01-2015

Given it is not the "same exception" when it gets wrapped I think the wording I highlighted is deficient/misleading. I agree it would be awkward to repeat the wrapping information everywhere but as we continually hear, people tend to gloss over class-level docs, if they even read them at all. Perhaps the word "exception" could be a link back to that class-doc referring to this? And perhaps we could change "exception" to "associated exception" (open to suggestion for better adjective) to make it clearer there is more to this. Thanks.
06-01-2015

David: Thanks. As you note, this is working according to spec. The specs for thenCompose etc rely on the class-level docs (that you quote in part) to explain exceptional cases. None of this requires any change (not even whenComplete specs), but perhaps we should do something to better direct readers attention without duplicating the same wording dozens of times.
06-01-2015

I think this is "working as designed" though it may be a little obscure. Walking through the example: CompletableFuture<String> fe = new CompletableFuture<>(); fe.completeExceptionally(new RuntimeException()); CompletableFuture<String> fe_WhenComplete = fe.whenComplete( (r, e) -> System.out.println("fe.whenComplete: " + e)); As the original CF has completed exceptionally that exception is passed directly to BiConsumer that was passed to whenComplete - hence it reports the raw RuntimeException. whenComplete also returns a new CompletionStage that is dependent on the original CompleteableFuture. So when we do: fe_WhenComplete.whenComplete( (r, e) -> System.out.println("fe_WhenComplete.whenComplete: " + e)); This dependent stage will complete exceptionally as well because the original CF did, but because it is a dependent-stage the original exception is now wrapped in a CompletionException. From the docs for CompletionStage: "Two method forms support processing whether the triggering stage completed normally or exceptionally: Method whenComplete allows injection of an action regardless of outcome, otherwise preserving the outcome in its completion. Method handle additionally allows the stage to compute a replacement result that may enable further processing by other dependent stages. In all other cases, if a stage's computation terminates abruptly with an (unchecked) exception or error, then _all dependent stages requiring its completion complete exceptionally as well, with a CompletionException holding the exception as its cause_. " So in summary - original stage passed original exception; dependent stages get passed wrapped exception. However the docs for CompletionStage.whenComplete (etc) do not mention the wrapping: "Returns a new CompletionStage with the same result or exception as this stage ..." It should say something like: "Returns a new CompletionStage with the same result or exception as this stage (but with the exception wrapped by a CompletionException) ..."
05-01-2015

I think there was a recent discussion of this behaviour on the concurrency-interest list, but can't check details at this time. Will update tomorrow.
05-01-2015

Given that BiConsumer of whenComplete and the BiFunction of handle accept a Throwable it does strongly suggest that the throwable to be passed to those functions should be the cause of a CompletionException, and the CompletionException itself (or manifests) when thrown via say a CF.join/getNow.
05-01-2015

The CF.completeExceptionally internally creates an AltResult holding the throwable, T say, passed to it. No attempt is made to wrap it in a CompletionException. Thus any call to whenComplete or handle to such an exceptionally completed future will report T. Any future completion stage will report T wrapped in a CompletionException. The following reproduces the problem more clearly: CompletableFuture<String> fe = new CompletableFuture<>(); fe.completeExceptionally(new RuntimeException()); CompletableFuture<String> fe_WhenComplete = fe.whenComplete( (r, e) -> System.out.println("fe.whenComplete: " + e)); fe_WhenComplete.whenComplete( (r, e) -> System.out.println("fe_WhenComplete.whenComplete: " + e)); CompletableFuture<String> fe_handle = fe.handle( (r, e) -> { System.out.println("fe.handle: " + e); throw (RuntimeException) e; }); fe_handle.handle( (r, e) -> { System.out.println("fe_handle.handle: " + e); throw (RuntimeException) e; }); Which will output: fe.whenComplete: java.lang.RuntimeException fe_WhenComplete.whenComplete: java.util.concurrent.CompletionException: java.lang.RuntimeException fe.handle: java.lang.RuntimeException fe_handle.handle: java.util.concurrent.CompletionException: java.lang.RuntimeException
05-01-2015

Could reproduce this issue with JDK 8u25 and JDK 9ea.
05-01-2015