Home > database >  How to use only `handleAsync` to handle non-async exceptions as well?
How to use only `handleAsync` to handle non-async exceptions as well?

Time:10-06

Java's handleAsync will not handle exceptions which don't come from a completion stage, for example:

package my.package;

import java.util.concurrent.CompletableFuture;

public class Test {

  private CompletableFuture<String> throwWithoutCompletionStage() {
    throw new RuntimeException("error");
  }

  private CompletableFuture<String> getName() {
    // calls external API, does some work then throws
    throwWithoutCompletionStage();
    return CompletableFuture.completedFuture("name");
  }

  public CompletableFuture<String> process() {
    return getName()
        .thenApplyAsync(result -> {
          // do something
          return result;
        })
        .handleAsync((result, exception) -> {
          if (exception != null) {
            return result;
          } else {
            return null;
          }
        });
  }
}

When process is called handleAsync will not be executed but rather the exception will be propagated to process's caller which is a bit confusing because one would think that handleAsync would catch the exception. So in this case I would also need to wrap process in try/catch to really catch all exceptions which looks weird in my opinion and is also error-prone: we need to always remember to wrap methods which return CompletableFuture in both handleAsync and try/catch.

Is there a best practice to prevent this double exception catching? One solution I thought of is to call supplyAsync which would create a completion stage and then use handleAsync on it:

  public CompletableFuture<CompletableFuture<String>> process() {
     return CompletableFuture.supplyAsync(() -> getName())
        .handleAsync((result, exception) -> {
          if (exception != null) {
            return null;
          } else {
            return result;
          }
        });
  }

The problem with this code is that now process return type is CompletableFuture<CompletableFuture<String>> but also this seems redundant to wrap code in supplyAsync solely in order to have handleAsync catch all exceptions.

CodePudding user response:

If you don’t want to change the behavior of the getName() method, you have to use something like

public CompletableFuture<String> process() {
  return CompletableFuture.supplyAsync(this::getName, Runnable::run)
      .thenCompose(Function.identity())
      .thenApplyAsync(result -> {
        // do something
        return result;
      })
      .handleAsync((result, exception) -> {
        if (exception != null) {
          return result;
        } else {
          return null;
        }
      });
}

By using Runnable::run as Executor you ensure that the “async” operation is executed immediately in the caller thread, just like the direct invocation of getName() would do. Using .thenCompose(Function.identity()), you get a CompletableFuture<String> out of the CompletableFuture<CompletableFuture<String>>.

However, if getName() returns a completed future in the successful case, it should also return a future in the exceptional case. This may look like

private CompletableFuture<String> getName() {
  try {
    throwWithoutCompletionStage();
    return CompletableFuture.completedFuture("name");
  } catch(Throwable t) {
    return CompletableFuture.failedFuture(t);
  }
}

failedFuture has been introduced in Java 9. If you need a Java 8 compatible solution, you have to add such a factory method to your code base

public static <U> CompletableFuture<U> failedFuture(Throwable ex) {
  CompletableFuture<U> f = new CompletableFuture<>();
  f.completeExceptionally(ex);
  return f;
}

Or you integrate this logic into the getName() method:

private CompletableFuture<String> getName() {
  CompletableFuture<String> result = new CompletableFuture<>();
  try {
    throwWithoutCompletionStage();
    result.complete("name");
  } catch(Throwable t) {
    result.completeExceptionally(t);
  }
  return result;
}
  • Related