Exception propagation in java.util.concurrent.CompletableFuture

Deadly 提交于 2019-12-06 03:38:54

There is no such thing as “suppressing an exception”. When you invoke exceptionally, you are creating a new future, which will be completed with the result of the previous stage or the result of evaluating the function if the previous stage completed exceptionally. The previous stage, i.e. the future you’re invoking exceptionally on, is not affected.

This applies to all methods chaining a depend function or action. Each of these methods creates a new future, which will be completed as documented. None of them affects the existing future you’re invoking the method on.

Perhaps, it becomes much 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 dependent stage, an Integer, can’t supersede the result of the prerequisite stage, a String. These simply are two different futures with different results. And since calling join() on f1 queries for the result of the first stage, it isn’t dependent on f2 and hence, does not even wait for its completion. (That’s also the reason why the code waits for the end of all background activity at the end).

The usage of exceptionally is not different. It might be confusing that the next stage has the same type and even the same result in the non-exceptional case, but it doesn’t change the fact that there are two distinct stages.

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);

There seems to be a widespread mindset of the CompletableFuture chaining methods to be some kind of builder for a single future, which unfortunately is misleadingly wrong. Another pitfall is the following mistake:

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 explained, each chained method creates a new stage, so keeping a reference to the stage returned by the last chained method, i.e. the last stage, is suitable to get the final result. But canceling this stage will only cancel that last stage and none of the prerequisite stages. Also, after cancellation, the last stage does not depend on the other stages anymore, as it is already completed by cancellation and capable of reporting this exceptional result while the other, now unrelated stages are still being evaluated in the background.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!