3

I know the web is full of advice not to catch Throwables, but does it apply when working with CompleteableFutures? For example, in

    ExecutorService es = Executors.newFixedThreadPool(2);
    CompletableFuture<String> cf = new CompletableFuture<>();
    es.submit(() -> {
        try {
            cf.complete(getValue());
        } catch (Exception e) { // should I catch Throwable instead?
            cf.completeExceptionally(e);
        }
    });

Should I catch Throwable or Exception? If I catch Exception and Error is thrown, program will most likely go into deadlock.

Vytenis Bivainis
  • 2,308
  • 21
  • 28

1 Answers1

1

You are right in that not catching Throwable bears the risk of the CompletableFuture never getting completed. So this risk will justify catching Errors resp. all Throwables 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.

Holger
  • 285,553
  • 42
  • 434
  • 765