Backend Development 10 min read

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.

IT Services Circle
IT Services Circle
IT Services Circle
Common Pitfalls When Using CompletableFuture in Java

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 forever

Good 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 guaranteed

Good example: Use thenCompose or sequential thenApply to enforce order.

CompletableFuture
f1 = CompletableFuture.supplyAsync(() -> 1);
CompletableFuture
f2 = f1.thenApply(a -> a + 2);
f2.join(); // guaranteed order

For further reading, see the linked articles about thread pools, C++ features, TypeScript trends, and messaging systems.

JavaConcurrencyThreadPoolCompletableFutureAsyncExceptionHandling
IT Services Circle
Written by

IT Services Circle

Delivering cutting-edge internet insights and practical learning resources. We're a passionate and principled IT media platform.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.