Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Throwing exceptions from within CompletableFuture exceptionally clause

Tags:

java

I am having a problem dealing with exceptions throw from CompletableFuture methods. I thought it should be possible to throw an exception from within the exceptionally clause of a CompletableFuture. For example, in the method below, I expected that executeWork would throw a RuntimeException because I am throwing one in the various exceptionally clauses, however, this does not work and I'm not sure why.

public void executeWork() {

  service.getAllWork().thenAccept(workList -> {
    for (String work: workList) {
      service.getWorkDetails(work)
        .thenAccept(a -> sendMessagetoQueue(work, a))
        .exceptionally(t -> {
          throw new RuntimeException("Error occurred looking up work details");
        });
    }
  }).exceptionally(t -> {
    throw new RuntimeException("Error occurred retrieving work list");
  });
}
like image 776
user4184113 Avatar asked Sep 15 '25 14:09

user4184113


1 Answers

You're doing a few things wrong here (async programming is hard):

First, as @VGR noted, executeWork() will not throw an exception when things go bad - because all the actual work is done on another thread. executeWork() will actually return immediately - after scheduling all the work but without completing any of it. You can call get() on the last CompletableFuture, which will then wait for the work completion, or failure, and will throw any relevant exceptions. But that is force-syncing and considered an anti-pattern.

Secondly, you don't need to throw new RuntimeException() from the exceptionally() handle - that one is actually called with the correct error (t) in your case.

looking at an analogous synchronous code, your sample looks something like this:

try {
  for (String work : service.getAllWork()) {
    try {
      var a = service.getWorkDetails(work);
      sendMessageToQueue(work, a);
    } catch (SomeException e) {
      throw new RuntimeException("Error occurred looking up work details");
    }
  }
} catch (SomeOtherException e) {
  throw new RuntimeException("Error occured retrieving work list");
}

So as you can see, there's no benefit from catching the exceptions and throwing RuntimeException (which also hides the real error) instead of just letting the exceptions propagate to where you can handle them.

The purpose of an exceptionally() step is to recover from exceptions - such as putting default values when retrieving data from user or IO has failed, or similar things. Example:

service.getAllWork().thenApply(workList -> workList.stream()
    .map(work -> service.getWorkDetails(work)
        .thenAccept(a -> sendMessageToQueue(work, a)
        .exceptionally(e -> {
          reportWorkFailureToQueue(work, e);
          return null;
        })
     )
).thenCompose(futStream -> 
    CompletableFuture.allOf(futStream.toArray(CompletableFuture[]::new)))
.exceptionlly(e -> {
   // handle getAllWork() failures here, getWorkDetail/sendMessageToQueue
   // failures were resolved by the previous exceptionally and converted to null values
});
like image 100
Guss Avatar answered Sep 17 '25 03:09

Guss