Does an exception handler passed to CompletableFuture.exceptionally() have to return a meaningful value?

A correct corresponding transformation with CompletableFuture is:

CompletableFuture<String> future = CompletableFuture.supplyAsync(...);
future.thenAccept(this::handleResult);
future.exceptionally(t -> {
    log.error("Unexpected error", t);
    return null;
});

Another way:

CompletableFuture<String> future = CompletableFuture.supplyAsync(...);
future
    .whenComplete((r, t) -> {
        if (t != null) {
            log.error("Unexpected error", t);
        }
        else {
            this.handleResult(r);
        }
    });

The interesting part here is that you were chaining futures in your examples. The seemingly fluent syntax is actually chaining futures, but it seems you don't want that here.

The future returned by whenComplete might be interesting if you want to return a future that processes something with an internal future's outcome. It preserves the current future's exception, if any. However, if the future completed normally and the continuation throws, it'll complete exceptionally with the thrown exception.

The difference is that anything that happens after future completes will happen before the next continuation. Using exceptionally and thenAccept is equivalent if you're the future's end-user, but if you're providing a future back to a caller, either one will process without a completion notification (as if in the background, if you may), most probably the exceptionally continuation since you'll probably want the exception to cascade on further continuations.


Note, that exceptionally(Function<Throwable,? extends T> fn) also returns CompletableFuture<T>. So you can chain futher.

The return value of Function<Throwable,? extends T> is meant to produce fallback result for next chained methods. So you can for example get the value from Cache if it is unavailable from DB.

CompletableFuture<String> future = CompletableFuture<String>.supplyAsync(/*get from DB*/)
  .exceptionally((t) -> {
    log.error("Unexpected error", t);
    return "Fallback value from cache";
  })
  .thenAccept(this::handleResult);

If exceptionally would accept Consumer<T> instead of function, then how it could return a CompletableFuture<String> for chaining futher?

I think you want a variant of exceptionally which would return void. But unfortunately, no, there is no such variant.

So, in your case you can safely return any value from this fallback function, if you not return this future object and not use it futher in your code (so it can't be chained futher). Better not even assign it to a variable.

CompletableFuture<String>.supplyAsync(/*get from DB*/)
  .thenAccept(this::handleResult)
  .exceptionally((t) -> {
    log.error("Unexpected error", t);
    return null;
  });