CompletableFuture is missing method to map values even in case of errors?

Viewed 287

I have a program that makes HTTP requests, where I might need to try several different servers to get a successful response. The HTTP client is async, so I get CompletableFuture results when making requests.

I can easily try the "next" server when I get a bad response status code, for example:

return httpClient.sendAsync(request,
                HttpResponse.BodyHandlers.ofByteArray()
).thenCompose(response -> {
    if (response.statusCode() == 200) {
        return completedStage(...); // all ok, done!
    }

    // try the next server
    return callThisMethodAgain(...);
);

This works because thenCompose expects the lambda it's given to return another CompletableFuture so I can chain asynchronous computations.

However, if the server is not available, the HTTP client will throw an Exception and my lambda won't execute and the async chain terminates with that Exception.

I can see there are methods like handle and handleAsync, but they don't let me return yet another CompletableFuture, as thenCompose does. They are kind of analogous to thenApply (i.e. like stream's map rather than flatMap), so it seems to me CompletableFuture is missing the method that would be analogous to flatMap where I want to handle both success and failures from the previous async action in the chain?

Is that correct? Is there some workaround I am missing?

1 Answers

I have found a work around, which is to write the function myself and throw it into a Utils class on my project (my AsyncUtils keeps growing!).

Here it is, in case it might be useful to anyone else:

public static <T, U> CompletionStage<U> handlingAsync(CompletableFuture<T> future,
                                                      BiFunction<T, Throwable, CompletionStage<U>> handler) {
    var result = new CompletableFuture<U>();
    future.whenComplete((ok, error) -> {
        var handledResult = handler.apply(ok, error);
        handledResult.whenComplete((ok2, err2) -> {
            if (err2 != null) {
                result.completeExceptionally(err2);
            } else {
                result.complete(ok2);
            }
        });
    });
    return result;
}

I wrote a little unit test and it works well for my purposes.

EDIT: thanks to the other linked answers, the above can be much simplified to this:

public static <T, U> CompletionStage<U> handlingAsync(CompletableFuture<T> future,
                                                      BiFunction<T, Throwable, CompletionStage<U>> handler) {
    return future.handle(handler).thenCompose(x -> x);
}
Related