javajava-streamcompletable-future

How to throw a custom exception from CompletableFuture?


Question: how can I directly throw a custom exception from .exceptionally()?

List<CompletableFuture<Object>> futures =
    tasks.stream()
        .map(task -> CompletableFuture.supplyAsync(() -> businessLogic(task))
        .exceptionally(ex -> {
                if (ex instanceof BusinessException) return null;

                //TODO how to throw a custom exception here??
                throw new BadRequestException("at least one async task had an exception");
        }))
        .collect(Collectors.toList());

try {
    List<Object> results = futures.stream()
        .map(CompletableFuture::join)
        .collect(Collectors.toList());
} catch (CompletionException e) {
        if (e.getCause() instanceof RuntimeException) {
              throw (RuntimeException) e.getCause();
        }
        throw new RuntimeException(e.getCause());
}

Problem: I just always get a CompletionException whose ex.getCause() is instanceof BadRequestException.

Is that possible at all?


Solution

  • As said by Didier L, exceptions thrown by the functions (or generally exceptions that completed a CompletableFuture) are always wrapped in a CompletionException (unless they are already a CompletionException or CancellationException).

    But note that your code becomes much simpler when not even trying to translate the exception via exceptionally:

    List<CompletableFuture<Object>> futures =
        tasks.stream()
            .map(task -> CompletableFuture.supplyAsync(() -> businessLogic(task)))
            .collect(Collectors.toList());
    try {
        List<Object> results = futures.stream()
            .map(CompletableFuture::join)
            .collect(Collectors.toList());
    } catch (CompletionException e) {
        throw e.getCause() instanceof BusinessException?
            new BadRequestException("at least one async task had an exception"): e;
    }
    

    or

    … catch (CompletionException e) {
        throw e.getCause() instanceof BusinessException?
            new BadRequestException("at least one async task had an exception"):
            e.getCause() instanceof RuntimeException rte? rte: e;
    }
    

    Since exceptionally’s primary purpose is translating an exception to a non-exceptional result value, using it for translating the exception to another thrown exception was not the best fit anyway and it also needed an instanceof. So performing this translation in the catch clause saves you from another translation step.