CompletableFuture in Java

Salitha Chathuranga
15 min readMay 1, 2023

--

Let’s learn asynchronous programming

Hi all, I came back with a very very important article for developers! It’s actually about how to do asynchronous programming with Java. You may have already heard of Multi threading with Java. That is the base of the asynchronous behaviors in Java. But I’m not going to explain threads, but I will explain stuff beyond that. Let’s say an advanced version of thread executions with Java. CompletableFuture class was introduced in Java 8. Until then Java had only Future class which came in Java 5.

Let’s discuss in detail…

Synchronous Vs Asynchronous Programming

In Synchronous programming one task is executed at a time. After completion of that task, next task will be executed. So, it’s having blocking code.
But in Asynchronous programming multiple tasks are executed at the same time simultaneously based on thread availability. Simply it’s non blocking code since we are not waiting one task to finish to start the next.

Futures Vs CompletableFutures

I mentioned that we had Future before CompletableFuture came into picture. Future class represents a future result of asynchronous computation. It will have a result in future after completion.

Let’s take a simple code example.

public class FutureExample {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(5);
Future<String> f1 = executorService.submit(getCallable("Task 1"));
Future<String> f2 = executorService.submit(getCallable("Task 2"));
Future<String> f3 = executorService.submit(getCallable("Task 3"));
String s1 = f1.get();
System.out.println(s1);
String s2 = f2.get();
System.out.println(s2);
String s3 = f3.get();
System.out.println(s3);
executorService.shutdown();
}
private static Callable<String> getCallable(String taskName) {
return () -> "Task:::" + taskName + " => Thread:::" + Thread.currentThread().getName();
}
}

// output:
// Task:::Task 1 => Thread:::pool-1-thread-1
// Task:::Task 2 => Thread:::pool-1-thread-2
// Task:::Task 3 => Thread:::pool-1-thread-3

Here, ExecutorService is a way of binding a thread pool into our java program. There are bunch of pools defined in Java. I took newFixedThreadPool for simplicity. I have setup the pool with 5 threads. We can pass either Runnable or Callable to submit method. Since future.get() can be used to retrieve data, I used a callable(Callable will return something but Runnable does not). Simply f1, f2, f3 will be executed together. But when we call future.get()— it’s a blocking call and it will wait until the result completion of our future. This way we had some sort of async programming till Java 8.

Drawbacks had in Future 💥

  • We can not complete a future manually — Let’s say we call an API. Due to an issue we get an error. We need to return a cached response in that case. We can not do this with future.
  • Multiple futures can not be chained together — futures can not be chained or combined in a way where one future result is dependent on previous future result.
  • No exception handling — there is no proper way to deal with exception situations with futures.
  • Blocking — future.get() method will block the thread. So, it’s not completely asynchronous.

Providing answers to these issues, CompletableFuture came in Java 8 like a miracle. In summary, CompletableFuture provides a more flexible and powerful API for working with asynchronous computations than Future.

CompletableFuture in Java 8

Let’s start discussing the core stuff. CompletableFuture class is implementing both Future and CompletionStage interfaces.

public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {
...... ...... ......
}

It’s an extension of Future. It has a lot of methods where we can create, run, combine, chain multiple futures together and has a very descriptive way of error handling also.

When we use this class, behind the scene it’s like we are delegating the tasks into several threads. It actually uses the global ForkJoinPool => commonPool to execute the tasks in parallel. If we want, we can pass our own thread pool also.

Let’s discover CompletableFuture class methods…… 😎

runAsync

This method takes a Runnable as an argument and returns nothing. It’s a Void asynchronous method. The processing is done by a separate thread in the ForkJoinPool.commonPool().

public class RunAsyncExample {
public static void main(String[] args) {
Runnable runnable1 = () -> {
System.out.println("Hello from Task 1::" + Thread.currentThread().getName());
};
CompletableFuture<Void> taskCompletableFuture1 = CompletableFuture.runAsync(runnable);
System.out.println("Hello from Main::" + Thread.currentThread().getName());
taskCompletableFuture1.join();

Runnable runnable2 = () -> {
System.out.println("Hello from Task 2::" + Thread.currentThread().getName());
};
ExecutorService executorService = Executors.newCachedThreadPool();
CompletableFuture<Void> taskCompletableFuture2 = CompletableFuture.runAsync(runnable2, executorService);
taskCompletableFuture2.join();
executorService.shutdown();
}
}

// output:
// Hello from Main::main
// Hello from Task 1::ForkJoinPool.commonPool-worker-1
// Hello from Task 2::pool-1-thread-1

You can see the printed thread name right? We can see 3 kind of threads! What are they?

  • main — Main thread which runs our application
  • ForkJoinPool.commonPool-worker-1 — First task has been executed with the ForkJoinPool thread which is the default thread for CompletableFuture
  • pool-1-thread-1 — Second task execution has an additional parameter. That’s a thread pool I have defined. So, second task is not using the default thread pool anymore. It uses the cached thread pool we created.

Join method: It is an instance method of the CompletableFuture class. It is used to return the value when the future is complete or throws an unchecked exception if completed exceptionally. If the task involved in the completion of the CompletableFuture raises an exception, then this method throws a CompletionException with the underlying exception as its cause.

supplyAsync

This method takes a Supplier as an argument and returns CompletableFuture of expected result data type. The processing is done by a separate thread in the ForkJoinPool.commonPool().

If you need to learn more about Java 8 suppliers, please refer here: https://salithachathuranga94.medium.com/consumer-and-supplier-in-java-8-ec8cf2aea9cf

Let’s try out a sample code.

public class SupplyAsyncExample {
public static void main(String[] args) {
Supplier<String> supplier = () -> {
System.out.println("Hello from Task 1::" + Thread.currentThread().getName());
return "Hello from Task 1::" + Thread.currentThread().getName();
};
CompletableFuture<String> taskCompletableFuture = CompletableFuture.supplyAsync(supplier);
System.out.println("Hello from Main::" + Thread.currentThread().getName());
String value = taskCompletableFuture.join();
System.out.println("Value 1::" + value);

Supplier<String> supplier2 = () -> {
System.out.println("Hello from Task 2::" + Thread.currentThread().getName());
return "Hello from Task 1::" + Thread.currentThread().getName();
};
ExecutorService executorService = Executors.newCachedThreadPool();
CompletableFuture<String> taskCompletableFuture2 = CompletableFuture.supplyAsync(supplier2, executorService);
String value2 = taskCompletableFuture2.join();
System.out.println("Value 2::" + value2);
executorService.shutdown();
}
}

// output:
// Hello from Main::main
// Hello from Task 1::ForkJoinPool.commonPool-worker-1
// Value 1::Hello from Task 1::ForkJoinPool.commonPool-worker-1
// Hello from Task 2::pool-1-thread-1
// Value 2::Hello from Task 1::pool-1-thread-1

Always the CompletableFuture is running on a new worker thread. Main thread will execute independently without bothering whether the other asynchronous operations are completed or not. I have used our own thread pool for second task.

There are more things can be done with these futures. They will be explained below.

Callback Methods 💥

These methods are mainly used to chain set of futures and do whatever we want according to the scenario addressed.

Examples:

ThenApplyAsync, ThenAcceptAsync, ThenAcceptAsync, ThenRunAsync, ThenComposeAsync, ThenCombineAsync

🔴 NOTE:
Each of this kind of method has 3 versions. For an example,

  • thenApply(fn) — runs fn on a thread defined by the CompleteableFuture on which it is called, so you generally cannot know where this will be executed. It might immediately execute if the result is already available.
  • thenApplyAsync(fn) — runs fn on a environment-defined executor regardless of circumstances. For CompletableFuture this will generally be ForkJoinPool.commonPool().
  • thenApplyAsync(fn,exec) — runs fn on given executor instead of ForkJoinPool executor.

The main difference in these 3 versions will be how they gain thread control and execute — on which thread it will be executed. But remember! There is nothing in thenApplyAsync that is more asynchronous than thenApply from the contract of these methods. Both does the same job.

I will show it on next section 😎.

thenApplyAsync

This method takes a Function as an argument and returns CompletableFuture of transformed data. As an example, if you want to modify a string returned from a previous future and return, you can use this method. The processing is done by a separate thread in the ForkJoinPool.commonPool(). Let’s understand with an example.

public class ThenApplyExample {
public static void main(String[] args) {
CompletableFuture<String> taskCompletableFuture = CompletableFuture.supplyAsync(() -> {
System.out.println("Hello from Task 1::supplyAsync::" + Thread.currentThread().getName());
return "Hey";
});
System.out.println("Hello from Main::" + Thread.currentThread().getName());
CompletableFuture<String> stringCompletableFuture = taskCompletableFuture.thenApplyAsync(data -> {
System.out.println("Hello from Task 1::thenApplyAsync::" + Thread.currentThread().getName());
return data + " Developers!";
});
String result = stringCompletableFuture.join();
System.out.println(result);
}
}

// output:
// Hello from Main::main
// Hello from Task 1::supplyAsync::ForkJoinPool.commonPool-worker-1
// Hello from Task 1::thenApplyAsync::ForkJoinPool.commonPool-worker-1
// Hey Developers!

I have concatenated two strings using two futures. The first string was transformed into another. You can see right?

There’s one important point there. According to the system logs, it shows that both futures have been executed using the same thread.

What happens if we use ThenApply instead of ThenApplyAsync?

Just change the method name in the above code and see the logs.

// Hello from Main::main
// Hello from Task 1::supplyAsync::ForkJoinPool.commonPool-worker-1
// Hello from Task 1::thenApplyAsync::main
// Hey Developers!

It seems same right? Noooo!!! Look at the 3rd log line! Thread name has been changed to main instead of ForkJoinPool thread. 😮

So, async method was able to gain control, reuse and execute the future on the same thread where previous call was executed. But the other method could not access the same thread. But again, this will depends on how much time taken to execute futures also and how JVM is scheduling the threads on availability. Result of non async method can be varied based on that.

All callback methods will have the same behavior for async and non async methods. So, I will show it only here. Otherwise article will be lengthy. 😉

thenAcceptAsync

This method takes a Consumer as an argument and returns nothing. It’s also a Void asynchronous method. The processing is done by a separate thread in the ForkJoinPool.commonPool(). Let’s understand with an example.

public class ThenAcceptExample {
public static void main(String[] args) {
CompletableFuture<String> taskCompletableFuture = CompletableFuture.supplyAsync(() -> {
System.out.println("Hello from Task 1::supplyAsync::" + Thread.currentThread().getName());
return "Hey";
});
System.out.println("Hello from Main::" + Thread.currentThread().getName());
Consumer<String> consumer = (data) -> System.out.println(data + " Developers! Hello from Task 1::thenAcceptAsync::" + Thread.currentThread().getName());
taskCompletableFuture.thenAcceptAsync(consumer).join();
}
}

// output:
// Hello from Main::main
// Hello from Task 1::supplyAsync::ForkJoinPool.commonPool-worker-1
// Hey Developers! Hello from Task 1::thenAcceptAsync::ForkJoinPool.commonPool-worker-1

I have printed the data came from previous future with some more data, in the example. Since we are using Async method, we have the executor thread to execute the futures.

PS: Usually a Consumer takes something and do some operation without returning anything. If you need to learn more about Java 8 consumers, please refer here: https://salithachathuranga94.medium.com/consumer-and-supplier-in-java-8-ec8cf2aea9cf

thenRunAsync

This method takes a Runnable as an argument and returns nothing. It’s also a Void asynchronous method. The processing is done by a separate thread in the ForkJoinPool.commonPool(). Let’s understand with an example.

public class ThenRunExample {
public static void main(String[] args) {
CompletableFuture<String> taskCompletableFuture = CompletableFuture.supplyAsync(() -> {
System.out.println("Hello from Task 1::supplyAsync::" + Thread.currentThread().getName());
return "Hey";
});
System.out.println("Hello from Main::" + Thread.currentThread().getName());
Runnable runnable = () -> System.out.println("Finishing Task 1::thenRunAsync::" + Thread.currentThread().getName());
taskCompletableFuture.thenRunAsync(runnable).join();
}
}

// output:
// Hello from Task 1::supplyAsync::ForkJoinPool.commonPool-worker-1
// Hello from Main::main
// Finishing Task 1::thenRunAsync::ForkJoinPool.commonPool-worker-1

We can use this method to run any void method or print statement after futures has been completed.

thenComposeAsync

This method takes a Function as an argument and returns CompletableFuture of the expected result. It is used to chain two dependent futures sequentially. Let’s understand with an example.

Scenario:

We get customer information from an API and using that info we are calling payments API to get relevant payment details. Two calls are inter dependent.

public class ThenComposeExample {
private static void sleep(int seconds) {
try {
TimeUnit.SECONDS.sleep(seconds);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
private static CompletableFuture<Map<String, String>> getUserDetails() {
return CompletableFuture.supplyAsync(() -> {
sleep(5);
System.out.println("getUserDetails:::" + Thread.currentThread().getName());
return getUser();
});
}

private static Map<String, String> getUser() {
Map<String, String> user = new ConcurrentHashMap<>();
user.put("userId", "1234");
user.put("userName", "salitha");
user.put("phoneNo", "0777123456");
return user;
}

private static CompletableFuture<List<String>> getPayments(String userName) {
return CompletableFuture.supplyAsync(() -> {
sleep(7);
System.out.println("getPayments:::" + Thread.currentThread().getName());
return Arrays.asList(
"USER: " + userName + " => $100",
"USER: " + userName + " => $65"
);
});
}

public static void main(String[] args) {
long startTime = System.currentTimeMillis();
CompletableFuture<List<String>> paymentsFuture = getUserDetails()
.thenComposeAsync(userData -> {
return getPayments(userData.get("userName"));
});
System.out.println("Hello from Main::" + Thread.currentThread().getName());
sleep(4);
List<String> payments = paymentsFuture.join();
System.out.println(payments);
long endTime = System.currentTimeMillis();
System.out.println("Time taken::" + (endTime - startTime) / 1000);
}
}
// output:
// getUserDetails:::ForkJoinPool.commonPool-worker-1
// Hello from Main::main
// getPayments:::ForkJoinPool.commonPool-worker-2
// [USER: salitha => $100, USER: salitha => $65]
// Time taken::12

If you see the logs, we have run the two tasks getUserDetails and getPayments methods on separate threads. Here, worker-1 and worker-2 are the ForkJoinPool threads allocated. Even though we have use sleep for 5 + 7 + 4 = 16 seconds, it has been executed within 12 seconds resulting asynchronous behavior.

thenCombineAsync

This method takes a BiFunction as an argument and returns a CompletableFuture of the expected result. It is used to combine two independent futures parallel and combine some result. Let’s understand with an example.

Scenario:

We need to get a user email and weather report parallel and send an email to that user.

public class ThenCombineExample {
private static void sleep(int seconds) {
try {
TimeUnit.SECONDS.sleep(seconds);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
private static CompletableFuture<String> getWeather() {
return CompletableFuture.supplyAsync(() -> {
sleep(5);
System.out.println("getUserDetails:::" + Thread.currentThread().getName());
return "Sunny, Temperature: 28C";
});
}

private static CompletableFuture<String> getUserEmail() {
return CompletableFuture.supplyAsync(() -> {
sleep(5);
System.out.println("getUserEmail:::" + Thread.currentThread().getName());
return "john@gmail.com";
});
}

public static void main(String[] args) {
long startTime = System.currentTimeMillis();
CompletableFuture<String> weatherEmailFuture = getUserEmail()
.thenCombineAsync(getWeather(), (email, weather) -> {
System.out.println("Sending email to:::" + email + " Weather report => " + weather);
System.out.println("Sending email:::" + Thread.currentThread().getName());
return email + " => " + weather;
});
System.out.println("Hello from Main::" + Thread.currentThread().getName());
sleep(4);
String email = weatherEmailFuture.join();
System.out.println(email);
long endTime = System.currentTimeMillis();
System.out.println("Time taken::" + (endTime - startTime) / 1000);
}
}

// Hello from Main::main
// getUserEmail:::ForkJoinPool.commonPool-worker-1
// getUserDetails:::ForkJoinPool.commonPool-worker-2
// Sending email to:::john@gmail.com Weather report => Sunny, Temperature: 28C
// Sending email:::ForkJoinPool.commonPool-worker-1
// john@gmail.com => Sunny, Temperature: 28C
// Time taken::5

Following the same pattern, getUserEmail and getWeather methods have been executed on two separate threads. And it has reused the thread — worker-1 which was released. Even though we have used sleep for 5 + 5 + 4 = 14 seconds, it has been executed within 5 seconds with a great asynchronous behavior.

Aggregation Methods 💥

Please mind this word — aggregation is used by me just to differentiate methods. It’s not a standard classification! 😅

So far you know, we have several methods to deal with two futures, right?But how to deal with more than two futures? What are the methods available for this?

allOf and anyOf methods…

allOf

When you have multiple futures to deal and perform some action after all those futures are completed only, this is the method to use. It returns a new CompletableFuture object when all of the specified CompletableFutures are complete. So, this method accepts a list of completablefutures. We can define and run them parallel and provide the futures into allOf method. If any of the specified CompletableFutures is completed with an exception, the resulting CompletableFuture does as well, with a CompletionException as the cause. Let’s understand with an example.

public class AllOfExample {
private static void sleep(int seconds) {
try {
TimeUnit.SECONDS.sleep(seconds);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}

private static CompletableFuture<String> futureOne() {
return CompletableFuture.supplyAsync(() -> {
System.out.println("futureOne::" + Thread.currentThread().getName());
sleep(4);
return "CF1";
});
}

private static CompletableFuture<String> futureTwo() {
return CompletableFuture.supplyAsync(() -> {
System.out.println("futureTwo::" + Thread.currentThread().getName());
sleep(3);
return "CF2";
});
}

private static CompletableFuture<String> futureThree() {
return CompletableFuture.supplyAsync(() -> {
System.out.println("futureThree::" + Thread.currentThread().getName());
sleep(2);
return "CF3";
});
}

public static void main(String[] args) {
long startTime = System.currentTimeMillis();
List<CompletableFuture<String>> completableFutures = Arrays.asList(futureOne(), futureTwo(), futureThree());
CompletableFuture<Void> future = CompletableFuture.allOf(completableFutures.toArray(new CompletableFuture[0]));
System.out.println("Hello from Main::" + Thread.currentThread().getName());
sleep(6);
CompletableFuture<List<String>> allFutureResults = future
.thenApply(t -> completableFutures.stream().map(CompletableFuture::join)
.collect(Collectors.toList()));
System.out.println("Result: " + allFutureResults.join());
long endTime = System.currentTimeMillis();
System.out.println("Time taken::" + (endTime - startTime) / 1000);
}
}

// output:
// Hello from Main::main
// futureTwo::ForkJoinPool.commonPool-worker-2
// futureOne::ForkJoinPool.commonPool-worker-1
// futureThree::ForkJoinPool.commonPool-worker-3
// Result: [CF1, CF2, CF3]
// Time taken::6

The futures I have defined has been executed on 3 separate worker threads as per the logs. And if you execute the code, you will see they are executed parallel and not one after the other. Even though we have used sleep for 4 + 3 + 2 + 6 = 15 seconds, it has been executed within 6 seconds. That’s how asynchronous code behaves.

anyOf

When you have several asynchronous tasks and you want to return a result as soon as 1 future is completed, this is the method you would choose. The anyOf() method gets tricky when a list of completableFutures return multiple types of outcomes. Due to this, the user is not able to tell which future got completed first. Let’s understand with an example.

public class AnyOfExample {
private static void sleep(int seconds) {
try {
TimeUnit.SECONDS.sleep(seconds);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}

private static CompletableFuture<String> futureOne() {
return CompletableFuture.supplyAsync(() -> {
System.out.println("futureOne::" + Thread.currentThread().getName());
sleep(4);
return "CF1";
});
}

private static CompletableFuture<String> futureTwo() {
return CompletableFuture.supplyAsync(() -> {
System.out.println("futureTwo::" + Thread.currentThread().getName());
sleep(3);
return "CF2";
});
}

private static CompletableFuture<String> futureThree() {
return CompletableFuture.supplyAsync(() -> {
System.out.println("futureThree::" + Thread.currentThread().getName());
sleep(2);
return "CF3";
});
}

public static void main(String[] args) {
long startTime = System.currentTimeMillis();
List<CompletableFuture<String>> completableFutures = Arrays.asList(futureOne(), futureTwo(), futureThree());
CompletableFuture<Object> future = CompletableFuture.anyOf(completableFutures.toArray(new CompletableFuture[0]));
System.out.println("Hello from Main::" + Thread.currentThread().getName());
System.out.println(future.join());;
long endTime = System.currentTimeMillis();
System.out.println("Time taken::" + (endTime - startTime) / 1000);
}
}

// output:
// futureTwo::ForkJoinPool.commonPool-worker-2
// futureOne::ForkJoinPool.commonPool-worker-1
// Hello from Main::main
// futureThree::ForkJoinPool.commonPool-worker-3
// CF3
// Time taken::2

Let’s understand the logs first. As always, futures are executed on separate threads. But what is the result giving by future.join()? It is “CF3”. How it has happened? If you see the 3 completablefuture methods, futureThree method has the less time of execution since I have slept it for 2 seconds. As soon as it is completed, we have the result! That’s why we got “CF3”.

Handling Exceptions 💥

There are 3 ways to handle exceptions while executing completable futures. Let’s look into them.

1️⃣ handle

Takes a BiFunction — result and exception which is executed when the stage completes either successfully or exceptionally. It does not matter whether program is executed properly or not. I will quickly show how it works using an erroneous scenario.

public class HandleExample {
public static void main(String[] args) {
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
int x = 10;
return x / 0;
}).handle((result, error) -> {
if (error != null) {
System.out.println("Error occurred!: " + error.getMessage());
return 0;
}
return result;
});
System.out.println(future.join());;
}
}

// output:
// Error occurred!: java.lang.ArithmeticException: / by zero
// 0

This future will definitely throw an Exception: Arithmetic Exception since I’m trying to divide by zero. So, handle method will catch that exception in the BiFunction and we can log the error message. If error is NOT NULL, we are returning zero, Otherwise result from our future. Since we have result and error at the same time, we have to perform a NULL check for one item. If I change the statement by replacing with this => return x / 2, then you will see the result as 5.

We can modify this example by chaining a callback method.

public class HandleExample {
public static void main(String[] args) {
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
int x = 10;
return x / 2;
}).handle((result, error) -> {
if (error != null) {
System.out.println("Error occurred!: " + error.getMessage());
return 5;
}
return result;
}).thenApplyAsync(x -> x + 20);
System.out.println(future.join());
}
}

// output:
// Error occurred!: java.lang.ArithmeticException: / by zero
// 25

Output is 25 since return 5 when there’s an exception. So, thenApplyAsync will take the result and add 20 and return.

This is one way that we can handle exceptions occurs while futures are executed.

2️⃣ exceptionally

Takes a Function — exception which is executed when the stage completes exceptionally. We will only get the error and not the result as before. I will use the same scenario above to demonstrate this method.

public class ExceptionallyExample {
public static void main(String[] args) {
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
int x = 10;
return x / 0;
}).exceptionally(error -> {
System.out.println("Error occurred!: " + error.getMessage());
return 0;
});
System.out.println(future.join());
}
}

// output:
// Error occurred!: java.lang.ArithmeticException: / by zero
// 0

Here, we only have the access to the exception. It does not catch the result. Output will be same as above. Both methods are catching exceptions, but in different ways.

We can modify this example also by chaining a callback method.

public class ExceptionallyExample {
public static void main(String[] args) {
CompletableFuture.supplyAsync(() -> {
int x = 10;
return x / 0;
}).exceptionally(error -> {
System.out.println("Error occurred!: " + error.getMessage());
return 0;
}).thenAcceptAsync(x -> {
System.out.println(x + 10);
});
}
}

// output:
// Error occurred!: java.lang.ArithmeticException: / by zero
// 10

Output became 10 since return zero when there’s an exception. So, thenAcceptAsync will take the result and add 10 to it and print.

3️⃣ whenComplete

This also takes a BiFunction — result and exception which is executed when the stage completes either successfully or exceptionally.

public class WhenCompleteExample {
public static void main(String[] args) {
CompletableFuture.supplyAsync(() -> {
int x = 10;
return x / 2;
}).whenComplete((result, error) -> {
if (error != null) {
System.out.println("Error occurred!: " + error.getMessage());
} else {
System.out.println(result);
}
});
}
}

// output:
// Error occurred!: java.lang.ArithmeticException: / by zero

If we chain a callback method after this, it won’t behave like exceptionally and handle methods. We cannot return a result a value inside this whenComplete method. That’s the reason.

✅ All done! Exceptions are handled 😎.

We have just finished the basic but essential guide on CompletableFuture in Java😃. This is too lengthy since I wanted to cover as much as I can. Actually it took 2️⃣ days to write this with examples. 😃

Asynchronous programming is very important and useful while we write real world applications. For an example, any database call will take some time to execute. But if we need to execute an API call without depending on the previous DB call? Then we have to delegate tasks into threads. Without going for naïve approach using thread pools and executors, we can use CompletableFutures! Otherwise the same thread will be blocked. At the end, user will experience a latency.

I will definitely bring you a real scenario with a Spring Boot application next time. 🔥 😎

Till then bye guys! ❤️

--

--

Salitha Chathuranga

Associate Technical Lead at Sysco LABS | Senior Java Developer | Blogger