Java's handleAsync
will not handle exceptions which don't come from a completion stage, for example:
package my.package;
import java.util.concurrent.CompletableFuture;
public class Test {
private CompletableFuture<String> throwWithoutCompletionStage() {
throw new RuntimeException("error");
}
private CompletableFuture<String> getName() {
// calls external API, does some work then throws
throwWithoutCompletionStage();
return CompletableFuture.completedFuture("name");
}
public CompletableFuture<String> process() {
return getName()
.thenApplyAsync(result -> {
// do something
return result;
})
.handleAsync((result, exception) -> {
if (exception != null) {
return result;
} else {
return null;
}
});
}
}
When process
is called handleAsync
will not be executed but rather the exception will be propagated to process
's caller which is a bit confusing because one would think that handleAsync
would catch the exception. So in this case I would also need to wrap process
in try/catch to really catch all exceptions which looks weird in my opinion and is also error-prone: we need to always remember to wrap methods which return CompletableFuture
in both handleAsync
and try/catch.
Is there a best practice to prevent this double exception catching? One solution I thought of is to call supplyAsync
which would create a completion stage and then use handleAsync
on it:
public CompletableFuture<CompletableFuture<String>> process() {
return CompletableFuture.supplyAsync(() -> getName())
.handleAsync((result, exception) -> {
if (exception != null) {
return null;
} else {
return result;
}
});
}
The problem with this code is that now process
return type is CompletableFuture<CompletableFuture<String>>
but also this seems redundant to wrap code in supplyAsync
solely in order to have handleAsync
catch all exceptions.
CodePudding user response:
If you don’t want to change the behavior of the getName()
method, you have to use something like
public CompletableFuture<String> process() {
return CompletableFuture.supplyAsync(this::getName, Runnable::run)
.thenCompose(Function.identity())
.thenApplyAsync(result -> {
// do something
return result;
})
.handleAsync((result, exception) -> {
if (exception != null) {
return result;
} else {
return null;
}
});
}
By using Runnable::run
as Executor
you ensure that the “async” operation is executed immediately in the caller thread, just like the direct invocation of getName()
would do. Using .thenCompose(Function.identity())
, you get a CompletableFuture<String>
out of the CompletableFuture<CompletableFuture<String>>
.
However, if getName()
returns a completed future in the successful case, it should also return a future in the exceptional case. This may look like
private CompletableFuture<String> getName() {
try {
throwWithoutCompletionStage();
return CompletableFuture.completedFuture("name");
} catch(Throwable t) {
return CompletableFuture.failedFuture(t);
}
}
failedFuture
has been introduced in Java 9. If you need a Java 8 compatible solution, you have to add such a factory method to your code base
public static <U> CompletableFuture<U> failedFuture(Throwable ex) {
CompletableFuture<U> f = new CompletableFuture<>();
f.completeExceptionally(ex);
return f;
}
Or you integrate this logic into the getName()
method:
private CompletableFuture<String> getName() {
CompletableFuture<String> result = new CompletableFuture<>();
try {
throwWithoutCompletionStage();
result.complete("name");
} catch(Throwable t) {
result.completeExceptionally(t);
}
return result;
}