Java – Exception propagation in java.util.concurrent.CompletableFuture

Exception propagation in java.util.concurrent.CompletableFuture… here is a solution to the problem.

Exception propagation in java.util.concurrent.CompletableFuture

There are two pieces of code.

In the first one, we create CompletableFuture from a task that always throws some exception. Then we apply the “exceptionally” method to this future, followed by the “theAccept” method. We don’t assign the new future returned by the Accept method to any variable. Then we call “join” in the original future. What we see is that both the “exceptionally” method and the “thenAccept” are called. We see it because they print the appropriate lines in the output. But the exception is not suppressed by the “exception” method. In this case, suppressing the exception and giving us some default values is exactly what we expect from the “exception”.

In the second fragment, we do almost the same thing, but assign the newly returned future to the variable and call “join” on it. In this case, the exception is suppressed as expected.

From my perspective, for the first part, consistent behavior either doesn’t suppress exceptions and doesn’t call “exceptionally” and “thenAccept”, or exceptions call and suppress exceptions.

Why should we fall somewhere in between?

First fragment:

public class TestClass {
    public static void main(String[] args) {
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(TestClass::doSomethingForInteger);

future.exceptionally(e -> {
                    System.out.println("Exceptionally");
                    return 42;
                })
                .thenAccept(r -> {
                    System.out.println("Accept");
                });

future.join();
    }

private static int doSomethingForInteger() {
        throw new IllegalArgumentException("Error");
    }
}

Second fragment:

public class TestClass {
    public static void main(String[] args) {
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(TestClass::doSomethingForInteger);

CompletableFuture<Void> voidCompletableFuture = future.exceptionally(e -> {
            System.out.println("Exceptionally");
            return 42;
        })
                .thenAccept(r -> {
                    System.out.println("Accept");
                });

voidCompletableFuture.join();
    }

private static int doSomethingForInteger() {
        throw new IllegalArgumentException("Error");
    }
}

Solution

There is no such thing as “suppressing exceptions”. When you call exceptionally you are creating a new future, it will be done using the results of the previous phase or the results of the evaluation function if the previous phase completes abnormally. The previous stage, the future you are calling exceptionally, is not affected.

This applies to all methods that link dependent functions or operations. Each of these methods creates a new future, which will be done as recorded. None of them affect the existing future where you call the method.

Perhaps, it will be clearer with the following example:

CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> {
    LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
    return "a string";
});

CompletableFuture<Integer> f2 = f1.thenApply(s -> {
    LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(2));
    return s.length();
});

f2.thenAccept(i -> System.out.println("result of f2 = "+i));

String s = f1.join();
System.out.println("result of f1 = "+s);

ForkJoinPool.commonPool().awaitQuiescence(1, TimeUnit.DAYS);

Here, it should be clear that the result of the dependency phase, an Integer, cannot replace the result of the prerequisite phase, a String. These are just two different futures with different outcomes. Since join() is called on f1 to query the results of the first stage, it does not depend on f2 and, therefore, does not even wait for it to complete. (This is also why the code waits for all background activities to end at the end.)

There is no difference in usage of the exceptionally one. In the case of non-exception, it may be confusing that the next stage has the same type or even the same result, but this does not change the fact that there are two different phases.

static void report(String s, CompletableFuture<?> f) {
    f.whenComplete((i,t) -> {
        if(t != null) System.out.println(s+" completed exceptionally with "+t);
        else System.out.println(s+" completed with value "+i);
    });
}
CompletableFuture<Integer> f1 = CompletableFuture.supplyAsync(() -> {
    LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
    throw new IllegalArgumentException("Error for testing");
});
CompletableFuture<Integer> f2 = f1.exceptionally(t -> 42);

report("f1", f1);
report("f2", f2);

ForkJoinPool.commonPool().awaitQuiescence(1, TimeUnit.DAYS);

It seems that there is a common belief that the CompletableFuture linking method is some kind of single future builder, which unfortunately is a misleading error. Another pitfall is the following error:

CompletableFuture<?> f = CompletableFuture.supplyAsync(() -> {
    LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
    System.out.println("initial stage");
    return "";
}).thenApply(s -> {
    LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
    System.out.println("second stage");
    return s;
}).thenApply(s -> {
    LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
    System.out.println("third stage");
    return s;
}).thenAccept(s -> {
    System.out.println("last stage");
});

f.cancel(true);
report("f", f);

ForkJoinPool.commonPool().awaitQuiescence(1, TimeUnit.DAYS);

As mentioned earlier, each linked method creates a new stage, so keeping a reference to the stage returned by the last linked method (that is, the last stage) is appropriate to get the final result. However, canceling this phase only cancels the last phase, not any prerequisite phases. In addition, after cancellation, the last phase no longer depends on the other phases, since it has already been completed by cancellation and is able to report this anomalous result, while other stages that are now irrelevant are still evaluated in the background.

Related Problems and Solutions