Search code examples
javaexceptioncompletable-futurejava-threads

How does whenComplete() work in a chain of CompletionStages?


I thought I understood whenComplete but I'm not sure now. This question originated in another thread.

The way we work with futures in my company is by chaining them:

CompletionStage<Foo> getFoo() {
  // ...
  return barService.getBar()
      .thenCompose(bar -> {
        CompletionStage<Baz> baz = bazService.getBaz(bar);
        // ...
        return qux;
      })
      .thenApply(qux -> {
        CompletionStage<Quux> quux = quuxService.getQuux(qux);
        // ...
        return foo;
      });
}

qux and quux are apparently the metasyntactic variables that follow foo, bar, and baz.

Now let's say I wanted to send a confirmation email when foo has been gotten. I don't need the sending of this confirmation email to hold up the response to whatever client called getFoo. We use whenComplete for these scenarios:

CompletionStage<Foo> getFoo() {
  // ...
  return barService.getBar()
      .thenCompose(bar -> {
        CompletionStage<Baz> baz = bazService.getBaz(bar);
        // ...
        return qux;
      })
      .thenApply(qux -> {
        CompletionStage<Quux> quux = quuxService.getQuux(qux);
        // ...
        return foo;
      })                                     _
      .whenComplete((foo, ex) -> {.           |
          if (ex == null) {                   |
            emailService.sendEmail(foo);      | (NEW)
          }                                   |
      });                                    _|
}

Now I thought the action in whenComplete happened in a separate thread completely independently of the thread it originated from. In other words, I thought as soon as foo was found, it'd be on its way to the caller, no matter what happened inside the whenComplete. But in reality, when the email service had a problem and threw an exception, the exception propagated all they way up, i.e. getFoo threw an exception, even though foo was found succesfully.

I was pointed to the Javadoc for whenComplete, which indeed says:

Unlike method handle, this method is not designed to translate completion outcomes, so the supplied action should not throw an exception. However, if it does, the following rules apply: if this stage completed normally but the supplied action throws an exception, then the returned stage completes exceptionally with the supplied action's exception. Or, if this stage completed exceptionally and the supplied action throws an exception, then the returned stage completes exceptionally with this stage's exception.

So here's where I'm confused:

I thought the whole point of whenComplete was to allow the originating thread to continue on its way without having to wait for the action in whenComplete. If whether or not the chain will complete normally depends on the whenComplete action though, doesn't that mean the chain always has to wait to see how whenComplete completes? How is whenComplete helping at all, if that's true?

I'm sure that I'm thinking of something wrong / misunderstanding how futures work, but I don't know what.


Solution

  • .whenComplete is similar to a finally block. It's useful when you want to do something even if the previous stage has failed. For example, log an exception or clean up some resources without stopping the propagation of the failure.

    In your case, it seems that you want to send an email only when there are no failures. You could rewrite it as:

    .thenAccept(emailService::sendEmail);
    

    In this case if sendEmail returns void, .thenAccept will return when the method completes. But if sendEmail returns a CompletionStage<Void>, thenAccept will continue without waiting for the result (and failure or successes will be ignored).

    If you want to have control over the thread executors, you can use the .then*Async methods instead of the regular ones. Otherwise, the pipeline will run everything in the same thread (if possible, it's not always the case).