Reputation: 2144
I'm playing with CompletableFuture chains and stumbled upon a situation with unexpected behavior (for me, at least): if an exceptional CompletableFuture is passed in .thenCompose()
call, the resulting CompletableFuture will be finished with original exception wrapped in CompletionException
. It may be hard to understand without an example:
public static <T> CompletableFuture<T> exceptional(Throwable error) {
CompletableFuture<T> future = new CompletableFuture<>();
future.completeExceptionally(error);
return future;
}
public static void main(String[] args) {
CompletableFuture<Void> exceptional = exceptional(new RuntimeException());
exceptional
.handle((result, throwable) -> {
System.out.println(throwable);
// java.lang.RuntimeException
System.out.println(throwable.getCause());
// null
return null;
});
CompletableFuture
.completedFuture(null)
.thenCompose(v -> exceptional)
.handle((result, throwable) -> {
System.out.println(throwable);
// java.util.concurrent.CompletionException: java.lang.RuntimeException
System.out.println(throwable.getCause());
// java.lang.RuntimeException
return null;
});
}
Of course i was expecting to deal with the same RuntimeException
no matter how many transformations were before or after in chain. I have two questions:
Upvotes: 1
Views: 1469
Reputation: 20579
The JavaDoc for thenCompose()
is:
Returns a new CompletionStage that, when this stage completes normally, is executed with this stage as the argument to the supplied function. See the
CompletionStage
documentation for rules covering exceptional completion.
and the definition of the interface states:
[…] In all other cases, if a stage's computation terminates abruptly with an (unchecked) exception or error, then all dependent stages requiring its completion complete exceptionally as well, with a
CompletionException
holding the exception as its cause. […]
As thenCompose
returns a dependent stage, this is expected behavior.
In fact, the only cases in which you could have something else than a CompletionException
is when you complete a CompletableFuture
explicitly with methods like completeExceptionally()
, cancel()
etc. Even methods such as supplyAsync()
will wrap your exceptions.
I don't think there is any other option to access the original exception as it is already quite easy to unwrap it with getCause()
. If you really need to do that often, you could write a helper method such as:
public static <T, U> BiFunction<? super T, Throwable, ? extends U>
unwrappingCompletionException(BiFunction<? super T, Throwable, ? extends U> fn) {
return (t, u) -> {
if (u instanceof CompletionException) {
return fn.apply(t, u.getCause());
}
return fn.apply(t, u);
};
}
and use it as follows:
CompletableFuture
.completedFuture(null)
.thenCompose(v -> exceptional)
.handle(unwrappingCompletionException((result, throwable) -> {
[…]
}));
Upvotes: 3