Last transformation not executed on CompletableFuture when completeExceptionally is called

Viewed 156

I have following code:

public final class Start {

    private static final CountDownLatch FINAL_THREAD = new CountDownLatch(1);

    private static String getValue() {
        System.out.println("Waiting...");
        try {
            Thread.sleep(Duration.ofSeconds(1).toMillis());
            return "value";
        } catch (InterruptedException e) {
            return "interrupted";
        }
    }

    private static void whenComplete(String value, Throwable ex) {
        if (ex != null) {
            System.out.println("whenComplete Ex: " + ex);
        } else {
            System.out.println("whenComplete Value: " + value);
        }
    }

    private static String handle(String value, Throwable ex) {
        if (ex != null) {
            System.out.println("handle Ex: " + ex);
        } else {
            System.out.println("handle Value: " + value);
        }
        FINAL_THREAD.countDown();
        return value;
    }

    private static String peek(String value) {
        System.out.println("peek: " + value);
        return value;
    }

    private static CompletableFuture<String> createRequest() {
        System.out.println("Create....");
        return CompletableFuture.supplyAsync(Start::getValue)
                .thenApply(Start::peek)
                .handle(Start::handle)
                .whenComplete(Start::whenComplete);
    }

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        createRequest().completeExceptionally(new RuntimeException("TEST"));
        FINAL_THREAD.await();
    }

}

When I execute it I get output like this:

> Task :Start.main()
Create....
Waiting...
peek: value
handle Value: value

BUILD SUCCESSFUL in 10s

I don't understand why Start::whenComplete is not called when both Start::peek and Start::handle are. If I switch handle with whenComplete then Start::handle will not be called, but Start::whenComplete will. I would expect that Start::whenComplete will be called with RuntimeExeception in this case, while other stages will be executed with value provided by Start::getValue.

1 Answers

I think the documentation of CompletableFuture covers this, but let's slowly get to it, as it is not that trivial. First we need to refactor slightly your code:

private static CompletableFuture<String> createRequest() {
    System.out.println("Create....");
    CompletableFuture<String> one = CompletableFuture.supplyAsync(Start::getValue);
    CompletableFuture<String> two = one.thenApply(Start::peek);
    CompletableFuture<String> three = two.handle(Start::handle);
    CompletableFuture<String> four = three.whenComplete(Start::whenComplete);

    return four;
}

And let's slightly change your main too:

public static void main(String[] args) {
    CompletableFuture<String> f = createRequest();
    boolean didI = f.completeExceptionally(new RuntimeException("TEST"));
    System.out.println("have I completed it? : " + didI);
    LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(5));
}

Now let's carefully looks at this:

CompletableFuture<String> four = three.whenComplete(Start::whenComplete);

via the documentation of whenComplete:

Returns a new CompletionStage with the same result or exception as this stage, that executes the given action when this stage completes.

breaking it into smaller pieces:

Returns a new CompletionStage (four) with the same result or exception as this stage (three), that executes the given action (Start::whenComplete) when this stage (three) completes.

Who is supposed to execute Start::whenComplete? According to the documentation : four. When is it supposed to execute it? When three is completed.


According to your flow, before three is completed, you completeExceptionally your four. So when three is done, so is four - meaning it can't execute that Start::whenComplete (action); simply because it is already completed. Another way to think about it is that when your code reaches this line:

CompletableFuture<String> four = three.whenComplete(Start::whenComplete);

four is an un-completed future. It can be completed in two ways:

  • either when three is completed, thus triggering Start::whenComplete

  • externally (what you do with completeExceptionally)

Because you complete it externally before three is completed, it will not run that action.


If you chain an action to the future that you complete:

four.whenComplete(Start::whenComplete);

this is when you will see the desired output.

Related