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

David Moles picture David Moles · Jun 12, 2016 · Viewed 13.4k times · Source

I'm used to the ListenableFuture pattern, with onSuccess() and onFailure() callbacks, e.g.

ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
ListenableFuture<String> future = service.submit(...)
Futures.addCallback(future, new FutureCallback<String>() {
  public void onSuccess(String result) {
    handleResult(result);
  }
  public void onFailure(Throwable t) {
    log.error("Unexpected error", t);
  }
})

It seems like Java 8's CompletableFuture is meant to handle more or less the same use case. Naively, I could start to translate the above example as:

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

This is certainly less verbose than the ListenableFuture version and looks very promising.

However, it doesn't compile, because exceptionally() doesn't take a Consumer<Throwable>, it takes a Function<Throwable, ? extends T> -- in this case, a Function<Throwable, ? extends String>.

This means that I can't just log the error, I have to come up with a String value to return in the error case, and there is no meaningful String value to return in the error case. I can return null, just to get the code to compile:

  .exceptionally((t) -> {
    log.error("Unexpected error", t);
    return null; // hope this is ignored
  });

But this is starting to get verbose again, and beyond verbosity, I don't like having that null floating around -- it suggests that someone might try to retrieve or capture that value, and that at some point much later I might have an unexpected NullPointerException.

If exceptionally() took a Function<Throwable, Supplier<T>> I could at least do something like this --

  .exceptionally((t) -> {
    log.error("Unexpected error", t);
    return () -> { 
      throw new IllegalStateException("why are you invoking this?");
    }
  });

-- but it doesn't.

What's the right thing to do when exceptionally() should never produce a valid value? Is there something else I can do with CompletableFuture, or something else in the new Java 8 libraries, that better supports this use case?

Answer

acelent picture acelent · Jun 13, 2016

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.