### Question:
FutureUtils { public static <T> CompletableFuture<T> exceptionallyCompose( final CompletableFuture<T> future, final Function<Throwable, CompletionStage<T>> errorHandler) { final CompletableFuture<T> result = new CompletableFuture<>(); future.whenComplete( (value, error) -> { try { final CompletionStage<T> nextStep = error != null ? errorHandler.apply(error) : completedFuture(value); propagateResult(nextStep, result); } catch (final Throwable t) { result.completeExceptionally(t); } }); return result; }  static CompletableFuture<T> exceptionallyCompose(
      final CompletableFuture<T> future,
      final Function<Throwable, CompletionStage<T>> errorHandler); static void propagateResult(
      final CompletionStage<T> from, final CompletableFuture<T> to); static void propagateResult(
      final Supplier<CompletableFuture<T>> from, final CompletableFuture<T> to); static void propagateCancellation(
      final CompletableFuture<?> from, final CompletableFuture<?> to);  }### Answer:
@Test public void shouldComposeExceptionallyWhenErrorOccurs() { final Function<Throwable, CompletionStage<String>> errorHandler = mockFunction(); final CompletableFuture<String> input = new CompletableFuture<>(); final CompletableFuture<String> afterException = new CompletableFuture<>(); when(errorHandler.apply(ERROR)).thenReturn(afterException); final CompletableFuture<String> result = exceptionallyCompose(input, errorHandler); verifyZeroInteractions(errorHandler); assertThat(result).isNotDone(); input.completeExceptionally(ERROR); verify(errorHandler).apply(ERROR); assertThat(result).isNotDone(); afterException.complete("Done"); assertThat(result).isCompletedWithValue("Done"); }
@Test public void shouldComposeExceptionallyWhenErrorOccursAndComposedFutureFails() { final RuntimeException secondError = new RuntimeException("Again?"); final Function<Throwable, CompletionStage<String>> errorHandler = mockFunction(); final CompletableFuture<String> input = new CompletableFuture<>(); final CompletableFuture<String> afterException = new CompletableFuture<>(); when(errorHandler.apply(ERROR)).thenReturn(afterException); final CompletableFuture<String> result = exceptionallyCompose(input, errorHandler); verifyZeroInteractions(errorHandler); assertThat(result).isNotDone(); input.completeExceptionally(ERROR); verify(errorHandler).apply(ERROR); assertThat(result).isNotDone(); afterException.completeExceptionally(secondError); assertCompletedExceptionally(result, secondError); }
@Test public void shouldComposeExceptionallyWhenErrorOccursAndErrorHandlerThrowsException() { final Function<Throwable, CompletionStage<String>> errorHandler = mockFunction(); final CompletableFuture<String> input = new CompletableFuture<>(); final IllegalStateException thrownException = new IllegalStateException("Oops"); when(errorHandler.apply(ERROR)).thenThrow(thrownException); final CompletableFuture<String> result = exceptionallyCompose(input, errorHandler); verifyZeroInteractions(errorHandler); assertThat(result).isNotDone(); input.completeExceptionally(ERROR); verify(errorHandler).apply(ERROR); assertCompletedExceptionally(result, thrownException); }
@Test public void shouldNotCallErrorHandlerWhenFutureCompletesSuccessfully() { final Function<Throwable, CompletionStage<String>> errorHandler = mockFunction(); final CompletableFuture<String> input = new CompletableFuture<>(); final CompletableFuture<String> afterException = new CompletableFuture<>(); when(errorHandler.apply(ERROR)).thenReturn(afterException); final CompletableFuture<String> result = exceptionallyCompose(input, errorHandler); verifyZeroInteractions(errorHandler); assertThat(result).isNotDone(); input.complete("Done"); verifyZeroInteractions(errorHandler); assertThat(result).isCompletedWithValue("Done"); }