Michael Deardeuff
Michael Deardeuff

Reputation: 10697

CompletableFuture withFallback / handle only some errors

I'm receiving responses from a service call via CompletableFuture. I'd like to handle some known exceptions the service returns — such as optimistic concurrency control conflicts.

Here's what I've got. Is there a better way to do this which does not wrap the exceptions or use SneakyThrows? Wrapping exceptions would mean other exception handlers must inspect causal chains instead of merely using instanceof.

someService.call(request)
    .handle((response, error) -> {
        if (error == null)
            return CompletableFuture.completedFuture(response);
        else if (error instanceof OCCException)
            return CompletableFuture.completedFuture(makeDefaultResponse());

        CompletableFuture<Response> errorFuture = new CompletableFuture<>();
        errorFuture.completeExceptionally(error);
        return errorFuture;
    }).thenCompose(Function.identity());

Along the same vein, is there a way to replicated guava's withFallback without the wrap-unwrap?

CompletableFuture<T> withFallback(CompletableFuture<T> future,
                                  Function<Throwable, ? extends CompletableFuture<T>> fallback) {
    return future.handle((response, error) -> {
        if (error == null)
            return CompletableFuture.completedFuture(response);
        else
            return fallback.apply(error);
    }).thenCompose(Function.identity());
}


...
// Here's the first part of the question implemented using withFallback.
// It's a little cleaner, but it still requires wrapping as a future.
withFallback(someService.call(request), error -> {
    if (error instanceof OCCException)
        return CompletableFuture.completedFuture(makeDefaultResponse());

    CompletableFuture<Response> errorFuture = new CompletableFuture<>();
    errorFuture.completeExceptionally(error);
    return errorFuture;
});

For completeness, here is what it would look like if I allowed the exceptions to be wrapped. (I've got a unit test to verify the thrown exception propagates down the chain):

someService.call(request)
    .exceptionally(error -> {
        if (error instanceof OCCException)
            return makeDefaultResponse();
        else
            // wrap because error is declared as a checked exception
            throw new RuntimeException(error);
    });

Upvotes: 8

Views: 7428

Answers (2)

Holger
Holger

Reputation: 298153

The guava style function you asked for could be implemented like this:

static <T> CompletableFuture<T> withFallback(CompletableFuture<T> future,
                  Function<Throwable, ? extends CompletableFuture<T>> fallback) {
  return future.handle((response, error) -> error)
    .thenCompose(error -> error!=null? fallback.apply(error): future);
}

being both, more compact and saving resources by reusing the source future for the case we don’t want to do any transformations. But to give the caller the opportunity to do the same without the introduction of another helper method, it would be very useful to change the method and use a BiFunction which gets the source future as additional parameter:

static <T> CompletableFuture<T> withFallback(CompletableFuture<T> future,
  BiFunction<CompletableFuture<T>, Throwable, ? extends CompletableFuture<T>>
                                                                      fallback) {
    return future.handle((response, error) -> error)
      .thenCompose(error -> error!=null? fallback.apply(future,error): future);
}

then you can use it like this:

withFallback(someService.call(request), (f,t) -> t instanceof OCCException?
                  CompletableFuture.completedFuture(makeDefaultResponse()): f)

Upvotes: 11

Misha
Misha

Reputation: 28133

The only way I can think of is to define a helper method like this:

static <T, E extends Throwable> CompletableFuture<T> betterHandle(CompletableFuture<T> source, Class<E> excClass, Supplier<T> sup) {
    CompletableFuture<T> result = new CompletableFuture<>();
    source.whenComplete( (t, ex) -> {
        if (ex == null) {
            result.complete(t);
        } else if (excClass.isInstance(ex)) {
            result.complete(sup.get());
        } else {
            result.completeExceptionally(ex);
        }
    });
    return result;
}

It's not pretty, but it will allow you to avoid wrapping exceptions:

CompletableFuture<...> result = betterHandle(
    someService.call(request), 
    OCCException.class, 
    () -> makeDefaultResponse()
);

Upvotes: 5

Related Questions