How to get combined effects of whenComplete and thenCompose?

Viewed 4390

I am trying to come up with a CompletableFuture with the combined effects of whenComplete and thenCompose, specifically:

  1. Returns a CompletionStage instead of just a result, similar to thenCompose.
  2. Executes even when previous stage completes exceptionally, similar to whenComplete, and does not stop the exception from propagating.

This post is close to what I'm trying to achieve but I don't want to use handle which hides the exception. Thanks for any ideas.

1 Answers

I don't believe CompletionStage or CompletableFuture provides any single method for this. However, combining handle with thenCompose should do what you want, if I understand your requirements correctly.

A handle stage is executed whether the parent stage has completed normally or exceptionally and gives you access to the result or error, respectively. From this stage you could return another CompletionStage which would either be completed normally or exceptionally depending on what arguments the handle stage receives.

handle((T result, Throwable error) -> {
    if (error != null) {
        return CompletableFuture.<T>failedStage(error);
    } else {
        return processResult(result); // returns CompletionStage<T>
    }
});

Now you have a CompletionStage<CompletionStage<T>>. Now we execute a flat map operation by invoking thenCompose:

thenCompose(Function.identity());

Which gives us a CompletionStage<T>. This CompletionStage<T> will be whatever instance was returned by handle. If that instance was a failed stage then the exception is still propagated; otherwise, the result is passed to whatever stage is dependent on the thenCompose stage and processing continues normally.

You can see this with the following example:

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;

public class Main {

    public static void main(String[] args) {
        methodThatReturnsCompletionStage()
                .handle((result, error) -> {
                    if (error != null) {
                        return CompletableFuture.<String>failedStage(error);
                    } else {
                        return processResult(result);
                    }
                })
                .thenCompose(future -> {
                    System.out.println("#thenCompose invoked");
                    return future; // Identity function
                })
                .thenApply(result -> {
                    System.out.println("#thenApply invoked");
                    return result; // Identity function (exists to show intermediary stage)
                })
                .whenComplete((result, error) -> {
                    System.out.println("#whenComplete invoked");
                    if (error != null) {
                        error.printStackTrace(System.out);
                    } else {
                        System.out.println(result);
                    };
                });
    }

    private static CompletionStage<String> methodThatReturnsCompletionStage() {
        return CompletableFuture.completedStage("Hello");
        // return CompletableFuture.failedStage(new RuntimeException("OOPS"));
    }

    private static CompletionStage<String> processResult(String result) {
        return CompletableFuture.completedFuture(result + ", World!");
    }

}

This will result in each stage being invoked and an output of Hello, World!. But if you switch methodThatReturnsCompletionStage() to return the failed stage instead then thenApply is skipped (because the future has failed) and the exception is given to whenComplete (which, like handle, is invoked for both normal or exceptional completion).

Note: Everything above uses the CompletionStage interface directly but using CompletableFuture works just as well (and may be preferable).

Related