I've struggled with this a few times in the past years. There are much more methods for normal completion as opposed to exceptional completion in the CompletionStage interface.
I just want to handle an exceptional completion from a previous stage in the chain. For a problem that should be trivial, the options are:
doSomethingAsync()
.thenAccept(result -> handleResult(result))
// option 1
.exceptionally(throwable -> {
LOGGER.error(throwable);
return null;
})
// option 2
.whenComplete((result, throwable) -> {
if ( throwable != null ) {
LOGGER.error(throwable);
}
})
// option 3
.handle((result, throwable) -> {
if ( throwable != null ) {
LOGGER.error(throwable);
}
return null;
})
For reference of these methods: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/CompletionStage.html
Option 1
I have to return a value for the new stage to complete normally. What if I don't have a meaningful fallback value or I don't want to continue completing normally?
Option 2
I have to check if the stage completed exceptionally. In this case, this is too verbose for something that should be trivial. I also don't have any use for the result parameter.
Option 3
Has both the issues of option 1 and option 2.
Additionally, I have seen some smelly things happen which could have been avoided with a better API. For example when the last stage of the chain is:
.whenComplete((result, throwable) -> {
if ( throwable != null ) {
LOGGER.error(throwable);
}
handleResult(result); // <- what if an exception occurs here? This will not be handled.
})
Proposed solutions
Just as with .thenApply(…) and .thenAccept(…) I think that .exceptionally(…) should respectively have been .exceptionallyApply(…) and .exceptionallyAccept(…). Continuing normal completion without a value is a valid use case.
But then again, what if you don't want to continue normal completion? There might be future stages that expect a non-null value and you can't provide a meaningful fallback, you want to continue the exceptional flow. That's why I also suggest .whenExceptionally(…) which behaves the same as .whenComplete(…) but accepts a Consumer with only the throwable parameter. (It might also be nice to add a similar method for normal completion)
Please let me know what you think!