CompletableFuture in Java: A Complete Guide
CompletableFuture
in Java is a powerful class that simplifies asynchronous programming. It was introduced in Java 8 and is an advanced version of the Future
interface. With CompletableFuture
, we can write non-blocking code, which improves application performance.
In this guide, we’ll explore CompletableFuture
in detail, understand its key methods, and look at examples of when and how to use it.
1. What is CompletableFuture?
CompletableFuture
is a class that implements both Future and CompletionStage interfaces. It allows us to:
Run asynchronous tasks.
-
Add callbacks once a task is finished.
-
Combine multiple futures.
-
Perform exception handling in asynchronous flows.
2. Ways to Create CompletableFuture
(1) runAsync()
– Runs a task without returning a result (Runnable
)
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
System.out.println("Task running asynchronously in: " + Thread.currentThread().getName());
});
future.get(); // Waits for the result (blocks)
Output:
Task running asynchronously in: ForkJoinPool.commonPool-worker-1
(2) supplyAsync()
– Runs a task and returns a result (Supplier
)
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
return "Hello from CompletableFuture!";
});
System.out.println(future.get());
Output:
Hello from CompletableFuture!
3. Key Methods of CompletableFuture
(1) thenApply()
– Transforms the result
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Java")
.thenApply(name -> "Hello, " + name);
System.out.println(future.get()); // Hello, Java
(2) thenAccept()
– Consumes the result (no return value)
CompletableFuture.supplyAsync(() -> "Java")
.thenAccept(result -> System.out.println("Received: " + result));
// Received: Java
(3) thenRun()
– Runs an action after completion
CompletableFuture.supplyAsync(() -> "Java")
.thenRun(() -> System.out.println("Task Completed!"));
// Task Completed!
(4) thenCompose()
– Chains multiple futures
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hello")
.thenCompose(result -> CompletableFuture.supplyAsync(() -> result + " World"));
System.out.println(future.get()); // Hello World
(5) thenCombine()
– Combines results of two futures
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "World");
CompletableFuture<String> combined = future1.thenCombine(future2, (res1, res2) -> res1 + " " + res2);
System.out.println(combined.get()); // Hello World
(6) allOf()
– Waits for all futures to complete
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Task 1");
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "Task 2");
CompletableFuture<String> future3 = CompletableFuture.supplyAsync(() -> "Task 3");
CompletableFuture<Void> allFutures = CompletableFuture.allOf(future1, future2, future3);
allFutures.get(); // Waits until all tasks are complete
(7) anyOf()
– Proceeds when any one future completes
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); }
return "Result 1";
});
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "Result 2");
CompletableFuture<Object> anyFuture = CompletableFuture.anyOf(future1, future2);
System.out.println(anyFuture.get()); // Result 2 (finishes first)
(8) exceptionally()
– Handles errors
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
if (true) throw new RuntimeException("Error Occurred!");
return "Success";
}).exceptionally(ex -> "Handled: " + ex.getMessage());
System.out.println(future.get());
// Handled: java.lang.RuntimeException: Error Occurred!
(9) handle()
– Handles both success and failure
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
return "Success";
}).handle((result, ex) -> {
if (ex != null) return "Error: " + ex.getMessage();
else return result;
});
System.out.println(future.get()); // Success
4. When to Use CompletableFuture?
-
To run asynchronous tasks (e.g., API calls, database operations).
-
For parallel processing with multiple independent tasks.
-
For callback-based workflows.
-
For complex task chaining (where the output of one task becomes the input of another).
5. Full Example: Multiple Asynchronous Tasks
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
public class CompletableFutureDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// Task 1: Fetch user name
CompletableFuture<String> getUserName = CompletableFuture.supplyAsync(() -> {
try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
return "Rahul";
});
// Task 2: Fetch user email
CompletableFuture<String> getUserEmail = CompletableFuture.supplyAsync(() -> {
try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); }
return "rahul@example.com";
});
// Combine both results
CompletableFuture<String> combinedFuture = getUserName.thenCombine(getUserEmail,
(name, email) -> "Name: " + name + ", Email: " + email);
System.out.println(combinedFuture.get());
// Output: Name: Rahul, Email: rahul@example.com
}
}
6. Conclusion
CompletableFuture
makes asynchronous programming in Java simpler and more powerful. With it, you can:
✔ Write non-blocking code.
✔ Combine multiple tasks easily.
✔ Handle exceptions gracefully.
✔ Add flexible callbacks for task completion.
It is widely useful in multithreaded applications, REST API calls, and database operations.