Java 8 introduced a lot of awesome features, one of them being CompletableFuture class.

Beside implementing the Future interface, CompletableFuture also implements the CompletionStage interface. CompletionStage promises that the computation eventually will be done.

The great thing about the CompletionStage is that it offers a vast selection of methods that let us attach callbacks which will be executed on completion.

This way we can build programs in a non-blocking fashion. Let’s take few examples to see some use cases of CompletableFuture.

Table of contents

  1. The simplest asynchronous computation
  2. Attach a callback
  3. Chaining multiple callbacks
  4. Parallel callbacks
  5. Handling Exceptions using exceptionally
  6. Handling Exceptions using whenComplete
  7. Callback on multiple computations using thenCombine
  8. Callback on multiple computations using runAfterBoth
  9. Callback on either of computations using runAfterBoth

The simplest asynchronous computation

Let’s start with the basics — creating a simple asynchronous computation.

Supplier<Integer> heavyMethod = () -> {
	// Some heavy computation which eventually returns an Integer
	return 10;
};

CompletableFuture<Integer> asyncFunction = CompletableFuture.supplyAsync(heavyMethod);

/* Print the result returned by heavyMethod */
Integer result = asyncFunction.get();  
System.out.println(result);
10

Here, supplyAsync takes a Supplier containing the heavy code we want to execute asynchronously. Once, the execution of heavyMethod is completed, the result will be printed.

Well, we can take it further by attaching a call back method which will print the result once heavyMethod returns the result.

Attach a callback

callback executes after the asynchronous computation is done.

thenAccept is one one option to add a callback. It takes a Consumer — printer — which prints the result of the heavyMethod when it’s done.

// Heavy computation which eventually returns an Integer
Supplier<Integer> heavyMethod = () -> {
	return 10;
};

// Print the request
Consumer<Integer> printer = (x) -> {
	System.out.println(x);
};

CompletableFuture<?> asyncFunction = CompletableFuture.supplyAsync(heavyMethod)
                                                      .thenAccept(printer);

asyncFunction.get();
10

What if we want to pass values from one call back to another and so on…Like a chain? thenAccept won’t help here as it takes an input and returns nothing. In these cases, we can use another call back feature - thenApply. thenApply takes an input and returns an output.

thenApply takes a Function which accepts an input and returns a result.

Let’s put all these into a code to see how we can use these functionalities.

Chaining multiple callbacks

Let’s extend our earlier example by addig a new method which multiplies the input and returns the result - multiply

// Heavy computation which eventually returns an Integer
Supplier<Integer> heavyMethod = () -> {
	return 10;
};

// multiply the input and return the result
Function<Integer, Integer> multiply = (x) -> {
	return x * x;
};

// Print the request
Consumer<Integer> printer = (x) -> {
	System.out.println(x);
};

CompletableFuture<?> asyncFunction = CompletableFuture.supplyAsync(heavyMethod)
                                                      .thenApply(multiply)
                                                      .thenAccept(printer);

asyncFunction.get();
100

Here, the response of heavyMethod will be consumed by multiply and the response of multiply will be consumed by printer, which eventually prints the value 100.

Parallel callbacks

Let’s say, once our heavyMethod is completed, we want to add as well as multiply on the same result paralelly. This can be achieved using thenApplyAsync.

// Heavy computation which eventually returns an Integer
Supplier<Integer> heavyMethod = () -> {
	return 10;
};

// add
Function<Integer, Integer> add = (x) -> {
	return x + x;
};

// multiply
Function<Integer, Integer> multiply = (x) -> {
	return x * x;
};

// Print the request
Consumer<Integer> printer = (x) -> {
	System.out.println(x);
};

CompletableFuture<Integer> asyncFunction = CompletableFuture.supplyAsync(heavyMethod);

asyncFunction.thenApplyAsync(add).thenAccept(printer);
asyncFunction.thenApplyAsync(multiply).thenAccept(printer);

asyncFunction.get(); 
100
20

add and multiply will submitted as separate tasks to the ForkJoinPool.commonPool(). This results in both the add and multiply callbacks being executed when the preceding heavyMethod is completed.

Asynchronous version is a good option when we have multiple callbacks dependent on the same computation result.

Handling Exceptions using exceptionally

Let’s consider a scenario where heavyMethod might throw an exception. We can use exceptionally function to catch the exception and handle it gracefully. exceptionally is termed as a recovery method.

In the below example, we are returning a string value NOTHING TO PRINT in exceptionally method. NOTHING TO PRINT will be our recovery output when ever our heavyMethod fails.

// Heavy computation which eventually throws NULL POINTER EXCEPTION
Supplier<String> heavyMethod = () -> {
    return ((String) null).toUpperCase();
};

// Print the message
Consumer<String> printer = (x) -> {
    System.out.println("PRINT MSG: " + x);
};

CompletableFuture<Void> asyncFunction = CompletableFuture.supplyAsync(heavyMethod)

        .exceptionally(ex -> {
            System.err.println("heavyMethod threw an exception: " + ex.getLocalizedMessage());
            return "NOTHING TO PRINT";
        }).thenAccept(printer);

asyncFunction.get();
heavyMethod threw an exception: java.lang.NullPointerException
PRINT MSG: NOTHING TO PRINT

Handling Exceptions using whenComplete

whenComplete gives us more flexiblity to handle both exceptions and the results. In the below example, we are using whenComplete to print the exception to error console before exceptionally recoveres be sending a default message to printer.

// Heavy computation which eventually throws NULL POINTER EXCEPTION
Supplier<String> heavyMethod = () -> {
    return ((String) null).toUpperCase();
};

// Print the message
Consumer<String> printer = (x) -> {
    System.out.println("PRINT MSG: " + x);
};

CompletableFuture<Void> asyncFunction = CompletableFuture.supplyAsync(heavyMethod)

        .whenComplete((String result, Throwable ex) -> {
            if (ex != null) {
                System.err.println("heavyMethod threw an exception: " + ex.getLocalizedMessage());
            }
        }).exceptionally(ex -> {
            return "NOTHING TO PRINT";
        }).thenAccept(printer);

asyncFunction.get();
heavyMethod threw an exception: java.lang.NullPointerException
PRINT MSG: NOTHING TO PRINT

Callback on multiple computations using thenCombine

Sometimes it would be really helpful to be able to create a callback that is dependent on the result of two computations. This is where thenCombine becomes handy.

thenCombine allows us to register a BiFunction callback depending on the result of two CompletionStages.

To see how this is done, let’s in addition to finding a receiver also execute the heavy job of creating some content before sending a message.

// Heavy computation which eventually returns an Integer
Supplier<Integer> heavyMethod1 = () -> {
    return 10;
};

// Heavy computation which eventually returns an Integer
Supplier<Integer> heavyMethod2 = () -> {
    return 15;
};

// Print the request
Consumer<Integer> printer = (x) -> {
    System.out.println(x);
};

CompletableFuture<Integer> asyncFunction1 = CompletableFuture.supplyAsync(heavyMethod1);
CompletableFuture<Integer> asyncFunction2 = CompletableFuture.supplyAsync(heavyMethod2);

BiFunction<Integer, Integer, Integer> sum = (result1, result2) -> {
    return (result1 + result2);
};

CompletableFuture<Void> combinedFunction = asyncFunction1.thenCombine(asyncFunction2, sum).thenAccept(printer);

combinedFunction.get(); 
25

In the above example, we started two asynchronous methods — heavyMethod1 and heavyMethod2. thenCombine is used to trigger sum which takes the result of both asynchronous methods and returns the result. printer prints this result.

Callback on multiple computations using runAfterBoth

runAfterBoth is another variant of thenCombine. runAfterBoth takes a Runnable not caring about the actual values of the preceding computation — only that they both are actually complete.

// Heavy computation which eventually returns an Integer
Supplier<Integer> heavyMethod1 = () -> {
	return 10;
};

// Heavy computation which eventually returns an Integer
Supplier<Integer> heavyMethod2 = () -> {
	return 15;
};

CompletableFuture<Integer> asyncFunction1 = CompletableFuture.supplyAsync(heavyMethod1);
CompletableFuture<Integer> asyncFunction2 = CompletableFuture.supplyAsync(heavyMethod2);

Runnable sum = () -> {
	System.out.println("Heavy Load methods completed successfully");
};

CompletableFuture<Void> combinedFunction = asyncFunction1.runAfterBoth(asyncFunction2, sum);

combinedFunction.get();
Heavy Load methods completed successfully

Callback on either of computations using runAfterBoth

Let’s say we have two sources of finding car details either through carfax or autocheck. We can ask both and take the result from who ever returns first.

This can be achieved by acceptEither as seen below. the consumer carDetails will be executed when either carfax or autocheck returns the result.

// Heavy computation which eventually returns an car details
Supplier<String> carfax = () -> {
    return "2013 Toyota Corolla";
};

// Heavy computation which eventually returns an car details
Supplier<String> autocheck = () -> {
    return "2013 Toyota Corolla";
};

CompletableFuture<String> carfaxResult = CompletableFuture.supplyAsync(carfax);
CompletableFuture<String> autocheckResult = CompletableFuture.supplyAsync(autocheck);

Consumer<String> carDetails = (car) -> {
    System.out.println("Car details: " + car);
};

CompletableFuture<Void> either = carfaxResult.acceptEither(autocheckResult, carDetails);

either.get();
Car details: 2013 Toyota Corolla

This concludes the features of CompletableFuture class. I hope you like this aticle.

Your feedback is always appreciated. Happy coding!