Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

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?

like image 764
David Moles Avatar asked Jun 12 '16 03:06

David Moles


People also ask

What is exceptionally in CompletableFuture?

Using exceptionally() methodThis method returns a new CompletionStage that, when this stage completes with exception, is executed with this stage's exception as the argument to the supplied function. Otherwise, if this stage completes normally, then the returned stage also completes normally with the same value.

Does CompletableFuture throw exception?

The CompletableFuture. join() method is similar to the get method, but it throws an unchecked exception in case the Future does not complete normally. This makes it possible to use it as a method reference in the Stream.

Can a Completable future be null?

Since Void can not be instantiated, you can only complete a CompletableFuture<Void> with a null result, which is exactly what you also will get when calling join() on the future returned by allOf() once it has been successfully completed. CompletableFuture<Void> cf = CompletableFuture.

How do you handle exceptions in runAsync?

runAsync(() -> { //process and throw exception }, anInstanceOfTaskExecutor ) . thenRun(() -> {}) . exceptionally(exception -> { // do something, handle exception }) )); In this case, it will execute thenRun .


2 Answers

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.

like image 92
acelent Avatar answered Sep 27 '22 22:09

acelent


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;
  });
like image 38
Ruslan Stelmachenko Avatar answered Sep 27 '22 22:09

Ruslan Stelmachenko