9

Suppose I want to recover with some value if I get a specific exception, otherwise return the failed future with the exception. I would expect something like this:

public static void main(String[] args) {
    CompletableFuture
            .supplyAsync(FuturesExample::fetchValue)
            .exceptionally(throwable -> {
                if (throwable instanceof RuntimeException) {
                    return "All good";
                }
                throw throwable; // does not compile
            });
}

public static String fetchValue() {
    // code that potentially throws an exception
    return "value";
}

If the fetchValue function would throw a checked exception, I would like to handle it in the chained methods. I have tried both return throwable and throw throwable, but neither does compile. Do CompletableFutures offer any solution to this scenario? I am aware that the Function interface that is the parameter of the exceptionally method does not throw any exceptions - I would just like to return the already failed future in this case. I would like to find a solution using Java 8.

Ondra K.
  • 2,767
  • 4
  • 23
  • 39
  • Class `CompletableFuture` was first added in JDK 8 but was enhanced in JDK 9. I think that in order to provide you an appropriate answer, you need to indicate which JDK you are using. – Abra Apr 01 '19 at 11:32
  • Java 8 - I'll update the question accordingly. – Ondra K. Apr 01 '19 at 11:33
  • CompletableFuture.exceptionally is defined as returning a CompletableFuture, neither a String nor an exception (https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/CompletableFuture.html). This cannot compile. – Uwe Allner Apr 01 '19 at 11:39
  • @UweAllner You are wrong. `exceptionally(throwable -> "value")` is perfectly valid and works as expected. The function still returns `CompletableFuture`, the lambda is a parameter. – Ondra K. Apr 01 '19 at 11:42

2 Answers2

10

In this scenario, it is impossible to receive a checked exception as the previous stage is based on a Supplier, which is not allowed to throw checked exceptions.

So you could handle all unchecked exceptions and raise an AssertionError for the throwables that ought to be impossible:

CompletableFuture
    .supplyAsync(FuturesExample::fetchValue)
    .exceptionally(throwable -> {
        if (throwable instanceof RuntimeException) {
            return "All good";
        }
        if(throwable instanceof Error) throw (Error)throwable;
        throw new AssertionError(throwable);
    });

Otherwise, you may consider that subsequent stages, as well as callers of join() will get all exceptions except CompletionException and CancellationException wrapped in a CompletionException anyway. E.g. when I use

public static void main(String[] args) {
    CompletableFuture<String> f = CompletableFuture
        .supplyAsync(FuturesExample::fetchValue)
        .exceptionally(throwable -> {
            if(throwable instanceof RuntimeException) {
                throw (RuntimeException)throwable;
            }
            throw new Error();
        });
    f.whenComplete((s,t) -> {
        if(t != null) {
            System.err.println("in whenComplete handler ");
            t.printStackTrace();
        }
    });
    System.err.println("calling join()");
    f.join();
}
public static String fetchValue() {
    throw new IllegalStateException("a test is going on");
}

I get

in whenComplete handler 
java.util.concurrent.CompletionException: java.lang.IllegalStateException: a test is going on
    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.CompletableFuture$AsyncSupply.exec(CompletableFuture.java:1692)
    at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:290)
    at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1020)
    at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1656)
    at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1594)
    at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:177)
Caused by: java.lang.IllegalStateException: a test is going on
    at FuturesExample.fetchValue(FuturesExample.java:23)
    at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1700)
    ... 6 more
calling join()
Exception in thread "main" java.util.concurrent.CompletionException: java.lang.IllegalStateException: a test is going on
    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.CompletableFuture$AsyncSupply.exec(CompletableFuture.java:1692)
    at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:290)
    at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1020)
    at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1656)
    at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1594)
    at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:177)
Caused by: java.lang.IllegalStateException: a test is going on
    at FuturesExample.fetchValue(FuturesExample.java:23)
    at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1700)
    ... 6 more

So I can use CompletionException for wrapping arbitrary throwables, utilizing the fact that CompletionException does not get wrapped again. So if I use

public static void main(String[] args) {
    CompletableFuture<String> f = CompletableFuture
        .supplyAsync(FuturesExample::fetchValue)
        .exceptionally(throwable -> {
            if(throwable instanceof CompletionException)
                throwable = throwable.getCause();
            System.err.println("wrapping '"+throwable+"' inside exceptionally");
            throw new CompletionException(throwable);
        });
    f.whenComplete((s,t) -> {
        if(t != null) {
            System.err.println("in whenComplete handler ");
            t.printStackTrace();
        }
    });
    System.err.println("calling join()");
    f.join();
}
public static String fetchValue() {
    throw new IllegalStateException("a test is going on");
}

I get

wrapping 'java.lang.IllegalStateException: a test is going on' inside exceptionally
in whenComplete handler 
java.util.concurrent.CompletionException: java.lang.IllegalStateException: a test is going on
    at FuturesExample.lambda$main$0(FuturesExample.java:12)
    at java.base/java.util.concurrent.CompletableFuture.uniExceptionally(CompletableFuture.java:986)
    at java.base/java.util.concurrent.CompletableFuture$UniExceptionally.tryFire(CompletableFuture.java:970)
    at java.base/java.util.concurrent.CompletableFuture.unipush(CompletableFuture.java:589)
    at java.base/java.util.concurrent.CompletableFuture.uniExceptionallyStage(CompletableFuture.java:1002)
    at java.base/java.util.concurrent.CompletableFuture.exceptionally(CompletableFuture.java:2307)
    at FuturesExample.main(FuturesExample.java:8)
Caused by: java.lang.IllegalStateException: a test is going on
    at FuturesExample.fetchValue(FuturesExample.java:24)
    at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1700)
    at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.exec(CompletableFuture.java:1692)
    at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:290)
    at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1020)
    at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1656)
    at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1594)
    at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:177)
calling join()
Exception in thread "main" java.util.concurrent.CompletionException: java.lang.IllegalStateException: a test is going on
    at FuturesExample.lambda$main$0(FuturesExample.java:12)
    at java.base/java.util.concurrent.CompletableFuture.uniExceptionally(CompletableFuture.java:986)
    at java.base/java.util.concurrent.CompletableFuture$UniExceptionally.tryFire(CompletableFuture.java:970)
    at java.base/java.util.concurrent.CompletableFuture.unipush(CompletableFuture.java:589)
    at java.base/java.util.concurrent.CompletableFuture.uniExceptionallyStage(CompletableFuture.java:1002)
    at java.base/java.util.concurrent.CompletableFuture.exceptionally(CompletableFuture.java:2307)
    at FuturesExample.main(FuturesExample.java:8)
Caused by: java.lang.IllegalStateException: a test is going on
    at FuturesExample.fetchValue(FuturesExample.java:24)
    at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1700)
    at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.exec(CompletableFuture.java:1692)
    at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:290)
    at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1020)
    at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1656)
    at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1594)
    at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:177)

which differs slightly in the stack traces, but does not make a difference to the code receiving/catching the exception, as in either case, it’s a CompletionException wrapping an IllegalStateException.

So going back to the example of your question, you could use

CompletableFuture
    .supplyAsync(FuturesExample::fetchValue)
    .exceptionally(throwable -> {
        if (throwable instanceof RuntimeException) { // includes CompletionException
            return "All good";
        }
        throw new CompletionException(throwable);
    });

Since CompletionException is a RuntimeException, this code handles it and avoids wrapping a CompletionException in another CompletionException. Otherwise, the pattern would be

    .exceptionally(throwable -> {
        if (some condition) {
            return some value;
        }
        throw throwable instanceof CompletionException?
            (CompletionException)throwable: new CompletionException(throwable);
    });
Holger
  • 285,553
  • 42
  • 434
  • 765
  • Thank you for your answer! So basically there's no other way than wrapping the exception with an unchecked one and then handling it accordingly in the `exceptionally` clause and then accessing `Throwable#getCause`? – Ondra K. Apr 02 '19 at 14:10
  • A `CompletableFuture` can get completed by calling `completeExceptionally` with an arbitrary `Throwable`, so does the solution in the middle of [this answer](https://stackoverflow.com/a/43767613/2711488) for using a `Callable` work. However, for most cases, it’s an unnecessary complication, given that for subsequent consumers or when calling `join()`, you’ll get that throwable wrapped in a `CompletionException` anyway. – Holger Apr 02 '19 at 16:43
3

As holger wrote, this is normally not possible.
But, there is a trick with lombok and it's @SneakyThrows.

public static void main(String[] args) {
    CompletableFuture
            .supplyAsync(FuturesExample::fetchValue)
            .exceptionally(throwable -> {
                if (throwable instanceof RuntimeException) {
                    return "All good";
                }
                FutureExample.reThrow(throwable);
                // maybe a "return null" is necessary here (even when it is not reachable)
            });
}

public static String fetchValue() {
    // code that potentially throws an exception
    return "value";
}

@SneakyThrows // <- threat checked exceptions in method-body as unchecked
public static void reThrow(Throwable throwable) {
   throw throwable;
}

You can also archive it with ExceptionUtils.rethrow() (thanks vlp).

public static void main(String[] args) {
    CompletableFuture
            .supplyAsync(FuturesExample::fetchValue)
            .exceptionally(throwable -> {
                if (throwable instanceof RuntimeException) {
                    return "All good";
                }
               reThrow(throwable);
                // maybe a "return null" is necessary here (even when it is not reachable)
            });
}
akop
  • 5,981
  • 6
  • 24
  • 51
  • 1
    An alternative to lombok is `ExceptionUtils.rethrow()` from commons lang3. – vlp Jan 26 '23 at 09:29
  • a nicer syntax for rethrow is `throw (RuntimeException)ExceptionUtils.rethrow(ex);` (note that the invalid cast is never executed). This way it is much more obvious where the code path ends. – vlp Mar 10 '23 at 16:25