Hi there!

CompletionStage<T> and its subclasses (i.e. CompletableFuture<T>) represent
an either successful (T) or failed (Throwable). These states are logically
mutually exclusive as can also be seen from the documentation [1]:

> Two method forms (handle and whenComplete) support unconditional
> computation whether the triggering stage completed normally or
> exceptionally.


The canonical example given there also proves this:

> (result, exception) -> {
>    if (exception == null) {
>      // triggering stage completed normally
>    } else {
>      // triggering stage completed exceptionally
>    }
>  }


However, the provided methods used for handling both values imply that they
(from type-system standpoint) are not mutually exclusive: handle and
handleAsync use BiFunction<? extends T, Throwable, R> to handle the values
which seems to be problematic in most cases due to the following reasons:

   - The states are described as non mutually exclusive (both values
   co-exist) and null is used specially to treat the absence of the
   failure, so the signature of the parameters is something like (T, null)
   ^ (null, Throwable).
   - Boilerplate null-check is almost always present as it is required to
   check for the present variant, also leading to extra indentation in most
   scenarios

One might say that this can be solved by chaining thenApply[Async] and
exceptionally[Async] calls but this is not true, because the latter may not
want to handle the error produced by the former.

The same problem is true for whenComplete[Async] which is even harder to
replace with any other method: unlike it producing CompletionStage<T>,
thenAccept[Async] methods produce CompletionStage<Void> so the only way to
reach the same behaviour is by utilizing thenApply[Async] with Function<T>
returning its argument (also forcing value overwrites on the side of
implementation which normally would benefit from the fact of the value not
being changed).

It looks to me that it may be rational to add overloads to the mentioned
methods which would consume pairs of Functions and Consumers respectively:

   - <U> CompletionStage<U> handle(Function<? super T, ? extends U>
   resultFn, Function<Throwable, ? extends U> exceptionFn)
   - <U> CompletionStage<U> handleAsync(Function<? super T, ? extends U>
   resultFn, Function<Throwable, ? extends U> exceptionFn)
   - <U> CompletionStage<U> handleAsync(Function<? super T, ? extends U>
   resultFn, Function<Throwable, ? extends U> exceptionFn, Executor executor)
   - CompletionStage<T> whenComplete(Consumer<? super T> resultAction,
   Consumer<? super Throwable> exceptionAction)
   - CompletionStage<T> whenCompleteAsync(Consumer<? super T> resultAction,
   Consumer<? super Throwable> exceptionAction)
   - CompletionStage<T> whenCompleteAsync(Consumer<? super T> resultAction,
   Consumer<? super Throwable> exceptionAction, Executor executor)

In order to maintain backwards-compatibility, the default implementations
may simply delegate to the old ones doing the canonical null-check on the
exception and then delegating to the corresponding handlers, for example:
default <U> CompletionStage<U> handle(Function<? super T, ? extends U>
resultFn, Function<Throwable, ? extends U> exceptionFn) {
    return handle((result, exception) -> exception == null
? resultFn.apply(result) : exceptionFn.apply(exception));
}

It also looks strange to me that while whenComplete[Async] methods use ?
extends Throwable for the type of the exception, handle[Async] simply use
Throwable. In my opinion, the former is more appropriate for handle[Async],
thus it may be rational to apply this change to them. However, this may be
considered a breaking change so I suggest discussing it.

[1]:
https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/CompletionStage.html

~ Peter

Reply via email to