Just wrap the checked exception into a CompletionException
Another point to take into account with exception handling in CompletableFuture
when using completeExceptionally()
is that the exact exception will be available in handle()
and whenComplete()
but it will be wrapped in CompletionException
when calling join()
or when it is forwarded to any downstream stage.
A handle()
or exceptionally()
applied to a downstream stage will thus see a CompletionException
instead of the original one, and will have to look at its cause to find the original exception.
Moreover, any RuntimeException
thrown by any operation (including supplyAsync()
) is also wrapped in a CompletionException
, except if it is already a CompletionException
.
Considering this, it is better to play it on the safe side and have your exception handlers unwrap the CompletionException
s.
If you do that, there is no point anymore to set the exact (checked) exception on the CompletableFuture
and it is much simpler to wrap checked exceptions in CompletionException
directly:
Supplier<Integer> numberSupplier = () -> {
try {
return SupplyNumbers.sendNumbers();
} catch (Exception e) {
throw new CompletionException(e);
}
};
To compare this approach with Holger's approach, I adapted your code with the 2 solutions (simpleWrap()
is the above, customWrap()
is Holger's code):
public class TestCompletableFuture {
public static void main(String args[]) {
TestCompletableFuture testF = new TestCompletableFuture();
System.out.println("Simple wrap");
testF.handle(testF.simpleWrap());
System.out.println("Custom wrap");
testF.handle(testF.customWrap());
}
private void handle(CompletableFuture<Integer> future) {
future.whenComplete((x1, y) -> {
System.out.println("Before thenApply(): " + y);
});
future.thenApply(x -> x).whenComplete((x1, y) -> {
System.out.println("After thenApply(): " + y);
});
try {
future.join();
} catch (Exception e) {
System.out.println("Join threw " + e);
}
try {
future.get();
} catch (Exception e) {
System.out.println("Get threw " + e);
}
}
public CompletableFuture<Integer> simpleWrap() {
Supplier<Integer> numberSupplier = () -> {
try {
return SupplyNumbers.sendNumbers();
} catch (Exception e) {
throw new CompletionException(e);
}
};
return CompletableFuture.supplyAsync(numberSupplier);
}
public CompletableFuture<Integer> customWrap() {
CompletableFuture<Integer> f = new CompletableFuture<>();
ForkJoinPool.commonPool().submit(
(Runnable & CompletableFuture.AsynchronousCompletionTask) () -> {
try {
f.complete(SupplyNumbers.sendNumbers());
} catch (Exception ex) {
f.completeExceptionally(ex);
}
});
return f;
}
}
class SupplyNumbers {
public static Integer sendNumbers() throws Exception {
throw new Exception("test"); // just for working sake its not correct.
}
}
Output:
Simple wrap
After thenApply(): java.util.concurrent.CompletionException: java.lang.Exception: test
Before thenApply(): java.util.concurrent.CompletionException: java.lang.Exception: test
Join threw java.util.concurrent.CompletionException: java.lang.Exception: test
Get threw java.util.concurrent.ExecutionException: java.lang.Exception: test
Custom wrap
After thenApply(): java.util.concurrent.CompletionException: java.lang.Exception: test
Before thenApply(): java.lang.Exception: test
Join threw java.util.concurrent.CompletionException: java.lang.Exception: test
Get threw java.util.concurrent.ExecutionException: java.lang.Exception: test
As you'll notice, the only difference is that the whenComplete()
sees the original exception before thenApply()
in the customWrap()
case. After thenApply()
, and in all other cases, the original exception is wrapped.
The most surprising thing is that get()
will unwrap the CompletionException
in the "Simple wrap" case, and replace it with an ExecutionException
.