Common Pitfalls When Using CompletableFuture in Java
This article introduces the advantages of Java's CompletableFuture and systematically outlines six common pitfalls—including default thread‑pool issues, exception handling, timeout management, thread‑local context loss, callback hell, and task‑ordering problems—while providing correct code examples and best‑practice recommendations.
Introduction
Hello, I am Tianluo. In daily development we often use CompletableFuture , but it hides several traps that are easy to overlook; this article lists them.
Advantages of Using CompletableFuture
CompletableFuture is an asynchronous programming tool introduced in Java 8. Its core benefits are simplifying asynchronous task orchestration, improving code readability, and providing flexibility.
Below is a simple example that demonstrates how to fetch user basic information and user medal information concurrently.
public class FutureTest {
public static void main(String[] args) throws InterruptedException, ExecutionException, TimeoutException {
UserInfoService userInfoService = new UserInfoService();
MedalService medalService = new MedalService();
long userId = 666L;
long startTime = System.currentTimeMillis();
// Fetch user basic info
CompletableFuture
completableUserInfoFuture = CompletableFuture.supplyAsync(() -> userInfoService.getUserInfo(userId));
Thread.sleep(300); // Simulate other main‑thread work
// Fetch medal info
CompletableFuture
completableMedalInfoFuture = CompletableFuture.supplyAsync(() -> medalService.getMedalInfo(userId));
UserInfo userInfo = completableUserInfoFuture.get(2, TimeUnit.SECONDS); // Get result with timeout
MedalInfo medalInfo = completableMedalInfoFuture.get(); // Get result
System.out.println("Total time " + (System.currentTimeMillis() - startTime) + "ms");
}
}The code shows how easily asynchronous behavior can be achieved with CompletableFuture.
1. Pitfall: Default Thread‑Pool
CompletableFuture uses ForkJoinPool.commonPool() by default. If tasks block or run for a long time, the common pool may be exhausted, affecting other tasks.
Bad example:
CompletableFuture
future = CompletableFuture.runAsync(() -> {
// Simulate long‑running task
try {
Thread.sleep(10000);
System.out.println("Tianluo 666");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
future.join();Good example: Create a custom thread pool to avoid the bottleneck.
// 1. Manually create a thread pool (core parameters configurable)
int corePoolSize = 10; // core threads
int maxPoolSize = 10; // max threads (fixed size)
long keepAliveTime = 0L; // non‑core thread keep‑alive time
BlockingQueue
workQueue = new ArrayBlockingQueue<>(100);
RejectedExecutionHandler rejectionHandler = new ThreadPoolExecutor.AbortPolicy();
ExecutorService customExecutor = new ThreadPoolExecutor(
corePoolSize,
maxPoolSize,
keepAliveTime,
TimeUnit.MILLISECONDS,
workQueue,
rejectionHandler
);
// 2. Submit async task
CompletableFuture
future = CompletableFuture.runAsync(() -> {
try {
Thread.sleep(10000);
System.out.println("Tianluo 666");
System.out.println("Task completed");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, customExecutor);
// 3. Block until completion
future.join();2. Pitfall: Exception Handling
The exception mechanism of CompletableFuture differs from the traditional try...catch approach.
Good practice: Use exceptionally or handle to process exceptions.
CompletableFuture
future = CompletableFuture.supplyAsync(() -> {
throw new RuntimeException("Tianluo test exception!");
});
future.exceptionally(ex -> {
System.err.println("Exception: " + ex.getMessage());
return -1; // default value
}).join();Output:
Exception: java.lang.RuntimeException: Tianluo test exception!3. Pitfall: Timeout Handling
CompletableFuture itself does not support timeout; a long‑running task may cause the program to wait indefinitely.
Bad example (JDK 8): Using join() without a timeout.
CompletableFuture
future = CompletableFuture.supplyAsync(() -> {
try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); }
return 1;
});
future.join(); // blocks foreverGood example (JDK 8): Use get(timeout, TimeUnit) and cancel on TimeoutException .
CompletableFuture
future = CompletableFuture.supplyAsync(() -> {
try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); }
return 1;
});
future.join();
try {
Integer result = future.get(3, TimeUnit.SECONDS);
System.out.println("Result after 3 seconds: " + result);
} catch (TimeoutException e) {
System.out.println("Task timed out");
future.cancel(true);
} catch (Exception e) {
e.printStackTrace();
}Good example (JDK 9+): Use orTimeout or completeOnTimeout .
CompletableFuture
future = CompletableFuture.supplyAsync(() -> {
try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); }
return 1;
}).orTimeout(3, TimeUnit.SECONDS)
.exceptionally(ex -> {
System.err.println("Timeout: " + ex.getMessage());
return -1;
}).join();4. Pitfall: Thread‑Local Context Propagation
By default CompletableFuture does not propagate thread‑local variables, which may lead to lost context.
ThreadLocal
threadLocal = new ThreadLocal<>();
threadLocal.set("Main thread");
CompletableFuture.runAsync(() -> {
System.out.println(threadLocal.get()); // prints null
}).join();Correct approach: Use a custom executor and manually transfer the context.
ThreadLocal
threadLocal = new ThreadLocal<>();
threadLocal.set("Main thread");
ExecutorService executor = Executors.newFixedThreadPool(1);
CompletableFuture.runAsync(() -> {
threadLocal.set("Worker thread");
System.out.println(threadLocal.get()); // prints Worker thread
}, executor).join();5. Pitfall: Callback Hell
Callback hell refers to deeply nested asynchronous callbacks (e.g., thenApply , thenAccept ) that make code hard to read and maintain.
Bad example:
CompletableFuture.supplyAsync(() -> 1)
.thenApply(r -> { System.out.println("Step 1: " + r); return r + 1; })
.thenApply(r -> { System.out.println("Step 2: " + r); return r + 1; })
.thenAccept(r -> System.out.println("Step 3: " + r));Good example: Split logic into separate methods and chain them.
CompletableFuture
future = CompletableFuture.supplyAsync(() -> 1)
.thenApply(this::step1)
.thenApply(this::step2);
future.thenAccept(this::step3);
private int step1(int r) { System.out.println("Step 1: " + r); return r + 1; }
private int step2(int r) { System.out.println("Step 2: " + r); return r + 1; }
private void step3(int r) { System.out.println("Step 3: " + r); }6. Pitfall: Task Orchestration and Execution Order
If tasks have dependencies, improper composition may cause unexpected execution order.
Bad example:
CompletableFuture
f1 = CompletableFuture.supplyAsync(() -> 1);
CompletableFuture
f2 = CompletableFuture.supplyAsync(() -> 2);
CompletableFuture
result = f1.thenCombine(f2, (a, b) -> a + b);
result.join(); // order not guaranteedGood example: Use thenCompose or sequential thenApply to enforce order.
CompletableFuture
f1 = CompletableFuture.supplyAsync(() -> 1);
CompletableFuture
f2 = f1.thenApply(a -> a + 2);
f2.join(); // guaranteed orderFor further reading, see the linked articles about thread pools, C++ features, TypeScript trends, and messaging systems.
IT Services Circle
Delivering cutting-edge internet insights and practical learning resources. We're a passionate and principled IT media platform.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.