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<UserInfo> completableUserInfoFuture = CompletableFuture.supplyAsync(() -> userInfoService.getUserInfo(userId));
Thread.sleep(300); // Simulate other main‑thread work
// Fetch medal info
CompletableFuture<MedalInfo> 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
CompletableFutureuses 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<Void> 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<Runnable> 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<Void> 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<Integer> 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
CompletableFutureitself 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<Integer> 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<Integer> 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<Integer> 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<String> 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<String> 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<Integer> 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<Integer> f1 = CompletableFuture.supplyAsync(() -> 1);
CompletableFuture<Integer> f2 = CompletableFuture.supplyAsync(() -> 2);
CompletableFuture<Integer> result = f1.thenCombine(f2, (a, b) -> a + b);
result.join(); // order not guaranteedGood example: Use thenCompose or sequential thenApply to enforce order.
CompletableFuture<Integer> f1 = CompletableFuture.supplyAsync(() -> 1);
CompletableFuture<Integer> 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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
