1

I was under the impression that anything you put in whenComplete, happens on a thread totally separate from the thread that spawned it. But we had an incident today where, looking at our logs, it seems as though an exception thrown from within the whenComplete bubbled up as if the exception were thrown in a regular chain (e.g. thenApply, thenCompose, etc.), and got caught by an exceptionally (in the original chain).

Is that possible? How?

I'm hoping the answer is No, that's not possible, and there's something weird going on / we're misreading our logs or logic.

Andrew Cheong
  • 29,362
  • 15
  • 90
  • 145
  • If the `CompletableFuture` that you invoked `whenComplete` on is already completed, the `BiConsumer` will be executed inline, ie. the thread calling `whenComplete`. However, any exceptions thrown there will be caught and used to complete the returned `CompletableFuture`. What do you mean by "bubbled up"? – Sotirios Delimanolis Jul 26 '22 at 22:46

1 Answers1

2

The Javadoc for CompletableFuture says, "...this method is not designed to translate completion outcomes, so the supplied action should not throw an exception." But then it tells you what to expect if the action does throw an exception...

it seems as though an exception thrown from within the whenComplete bubbled up as if the exception were thrown in a regular chain.

...The Javadoc says, "if this stage completed normally but the supplied action throws an exception, then the returned stage completes exceptionally with the supplied action's exception."

That sounds like the behavior you are seeing.


Response to comment:

But if task B throwing an exception causes task A (upon completing normally) to throw task B's exception, then doesn't that mean task A has to wait on task B?

No. There's no waiting. A CompletableFuture can be completed in either of two ways: It can be completed normally (e.g., as if by complete(T value)), or it can be completed exceptionally (as if by completeExceptionally(Throwable ex)). In the "exceptionally" case, a reference to the Throwable ex object will be stored in the future object.

Some time later, when some other thread eventually calls future.get() it will return the stored value if the the future was normally completed, but if the future was exceptionally completed, then the get() call will throw new ExecutionException(ex) where ex was the original Throwable object that was stored in the future.

So really, there's two exceptions thrown; One to report whatever the original bad thing was that happened when it happened, and then another that is deferred until some other thread calls future.get(). In your example, the "bad thing" happened when some pool thread performed the action that you supplied to whenComplete(...,action).

Solomon Slow
  • 25,130
  • 5
  • 37
  • 57
  • Thanks! It was right in the Javadoc, of course. I have a question though. I thought the whole point of `whenComplete` was so that you don't hold up task A waiting for task B. But if task B throwing an exception causes task A (upon completing normally) to throw task B's exception, then doesn't that mean task A has to wait on task B? To see if it throws an exception? – Andrew Cheong Jul 27 '22 at 00:29
  • @AndrewCheong, No. See what I added to my answer, above. – Solomon Slow Jul 27 '22 at 12:25
  • Thanks again! I was a little uncareful with my words, although maybe you understood regardless. I meant: I thought the point of `whenComplete` was that I could spawn a totally unrelated task after Task A completed, but let Task A be "gotten" (_e.g._ return its response to the caller) without needing to know how Task B completed. If we're on the same page, then is your answer perhaps saying that it's a race condition? _e.g._ If some other thread eventually calls `future.get()` _before_ Task B throws an exception, then it will get the normally completed Task A; but if _after_, then it throws? – Andrew Cheong Jul 27 '22 at 12:36
  • 1
    @AndrewCheong, There's no race condition. `future.get()` always waits for the `future` to be completed before it either returns or throws. I don't know for sure what really is happening in your code because you only tried to describe it in words—you haven't showed any of it. But, it sounds like you are building a _chain_ of `CompletableFuture` objects. If you are unsure of how the "chaining" works, then that would be a good topic for a new StackOverflow question. – Solomon Slow Jul 27 '22 at 12:42