You are right in that not catching Throwable
bears the risk of the CompletableFuture
never getting completed. So this risk will justify catching Error
s resp. all Throwable
s which you normally wouldn’t.
But since you are basically reinventing supplyAsync
, let’s compare the behavior of the two:
public class CF {
public static void main(String[] args) {
ExecutorService es = Executors.newFixedThreadPool(2);
CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> getValue(), es);
try {
cf1.join();
}
catch(CompletionException ex) {
ex.printStackTrace();
}
System.err.println();
CompletableFuture<String> cf2 = new CompletableFuture<>();
es.submit(() -> {
try {
cf2.complete(getValue());
} catch(Throwable t) {
cf2.completeExceptionally(t);
}
});
try {
cf2.join();
}
catch(CompletionException ex) {
ex.printStackTrace();
}
}
private static String getValue() {
throw new OutOfMemoryError();
}
}
java.util.concurrent.CompletionException: java.lang.OutOfMemoryError
at java.base/java.util.concurrent.CompletableFuture.encodeThrowable(CompletableFuture.java:314)
at java.base/java.util.concurrent.CompletableFuture.completeThrowable(CompletableFuture.java:319)
at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1702)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
at java.base/java.lang.Thread.run(Thread.java:834)
Caused by: java.lang.OutOfMemoryError
at CF.getValue(CF.java:39)
at CF.lambda$main$0(CF.java:11)
at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1700)
... 3 more
java.util.concurrent.CompletionException: java.lang.OutOfMemoryError
at java.base/java.util.concurrent.CompletableFuture.reportJoin(CompletableFuture.java:412)
at java.base/java.util.concurrent.CompletableFuture.join(CompletableFuture.java:2044)
at CF.main(CF.java:31)
Caused by: java.lang.OutOfMemoryError
at CF.getValue(CF.java:39)
at CF.lambda$main$1(CF.java:24)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
at java.base/java.lang.Thread.run(Thread.java:834)
So we see, catching errors is the standard behavior of CompletableFuture
. Only the stack traces differ. Let’s see whether we can raise the convergence:
public class CF {
public static void main(String[] args) {
ExecutorService es = Executors.newFixedThreadPool(2);
CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> getValue(), es);
try {
cf1.join();
}
catch(CompletionException ex) {
ex.printStackTrace();
}
System.err.println();
CompletableFuture<String> cf2 = new CompletableFuture<>();
es.submit(() -> {
try {
cf2.complete(getValue());
} catch(Throwable t) {
cf2.completeExceptionally(new CompletionException(t));
}
});
try {
cf2.join();
}
catch(CompletionException ex) {
ex.printStackTrace();
}
}
private static String getValue() {
throw new OutOfMemoryError();
}
}
java.util.concurrent.CompletionException: java.lang.OutOfMemoryError
at java.base/java.util.concurrent.CompletableFuture.encodeThrowable(CompletableFuture.java:314)
at java.base/java.util.concurrent.CompletableFuture.completeThrowable(CompletableFuture.java:319)
at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1702)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
at java.base/java.lang.Thread.run(Thread.java:834)
Caused by: java.lang.OutOfMemoryError
at CF.getValue(CF.java:39)
at CF.lambda$main$0(CF.java:11)
at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1700)
... 3 more
java.util.concurrent.CompletionException: java.lang.OutOfMemoryError
at CF.lambda$main$1(CF.java:26)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
at java.base/java.lang.Thread.run(Thread.java:834)
Caused by: java.lang.OutOfMemoryError
at CF.getValue(CF.java:39)
at CF.lambda$main$1(CF.java:24)
... 5 more
That’s quiet close.
Of course, if getValue()
doesn’t throw checked exceptions, there is no reason not to use CompletableFuture.supplyAsync(() -> getValue(), es);
. If we have a reason to implement the completion manually, like having to handle checked exceptions, there are some things we can improve. If we don’t need the Future
returned by submit
, we can use execute
instead, to avoid the creation of an unneeded FutureTask
. Further, CompletableFuture
marks its completion tasks with AsynchronousCompletionTask
to help monitoring and debugging and its useful to follow the convention:
CompletableFuture<String> cf2 = new CompletableFuture<>();
es.execute((Runnable & CompletableFuture.AsynchronousCompletionTask)() -> {
try {
cf2.complete(getValue());
} catch(Throwable t) {
cf2.completeExceptionally(new CompletionException(t));
}
});
There is no direct consequence for the actual behavior though.