Completable futures. What's the best way to handle business "exceptions"?

Yuliban :

I'm just starting to get familiar with the CompletableFuture tool from Java. I've created a little toy application to model some recurrent use case almost any dev would face.

In this example I simply want to save a thing in a DB, but before doing so I want to check if the thing was already saved.

If the thing is already in the DB the flow (the chain of completable futures) should stop and not save the thing. What I'm doing is throwing an exception so eventually I can handle it and give a good message to the client of the service so he can know what happened.

This is what I've tried so far:

First the code that try to save the thing or throw an error if the thing is already in the table:

repository
        .query(thing.getId())
        .thenCompose(
            mayBeThing -> {
              if (mayBeThing.isDefined()) throw new CompletionException(new ThingAlreadyExists());
              else return repository.insert(new ThingDTO(thing.getId(), thing.getName()));

And this is the test I'm trying to run:

    CompletableFuture<Integer> eventuallyMayBeThing =
        service.save(thing).thenCompose(i -> service.save(thing));
    try {
      eventuallyMayBeThing.get();
    } catch (CompletionException ce) {
      System.out.println("Completion exception " + ce.getMessage());
      try {
        throw ce.getCause();
      } catch (ThingAlreadyExist tae) {
        assert (true);
      } catch (Throwable t) {
        throw new AssertionError(t);
      }
    }

This way of doing it I took it from this response: Throwing exception from CompletableFuture ( the first part of the most voted answer ).

However, this is not working. The ThingAlreadyExist is being thrown indeed but it's never being handled by my try catch block. I mean, this:

catch (CompletionException ce) {
      System.out.println("Completion exception " + ce.getMessage());
      try {
        throw ce.getCause();
      } catch (ThingAlreadyExist tae) {
        assert (true);
      } catch (Throwable t) {
        throw new AssertionError(t);
      }

is never executed.

I have 2 questions,

  1. Is there a better way?

  2. If not, am I missing something? Why can't I handle the exception in my test?

Thanks!

Update(06-06-2019)

Thanks VGR you are right. This is the code working:

try {
      eventuallyMayBeThing.get();
    } catch (ExecutionException ce) {
      assertThat(ce.getCause(), instanceOf(ThingAlreadyExists.class));
    }
Holger :

You have to be aware of the differences between get() and join().

The method get() is inherited from the Future interface and will wrap exceptions in an ExecutionException.

The method join() is specific to CompletableFuture and will wrap exceptions in a CompletionException, which is an unchecked exception, which makes it more suitable for the functional interfaces which do not declare checked exceptions.

That being said, the linked answer addresses use cases where the function has to do either, return a value or throw an unchecked exception, whereas your use case involves compose, where the function will return a new CompletionStage. This allows an alternative solution like

.thenCompose(mayBeThing -> mayBeThing.isDefined()?
    CompletableFuture.failedFuture​(new ThingAlreadyExists()):
    repository.insert(new ThingDTO(thing.getId(), thing.getName())))

CompletableFuture.failedFuture has been added in Java 9. If you still need Java 8 support, you may add it to your code base

public static <T> CompletableFuture<T> failedFuture(Throwable t) {
    final CompletableFuture<T> cf = new CompletableFuture<>();
    cf.completeExceptionally(t);
    return cf;
}

which allows an easy migration to a newer Java version in the future.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=90084&siteId=1