Calling CompletableFuture.get()
can be dangerous in production environments if used incorrectly or in the wrong context. While get()
is a convenient way to retrieve the result of an asynchronous computation, it comes with several risks that can hurt performance, responsiveness, and reliability of your application, especially in highly concurrent or reactive systems.
Below is a detailed explanation of why CompletableFuture.get()
can be problematic in production and what you should do instead.
1. Blocking the Current Thread
Problem:CompletableFuture.get()
is a blocking call. It causes the calling thread to wait indefinitely until the computation completes or an exception is thrown.
Why it’s dangerous:
- In web applications, blocking an I/O thread (e.g., Tomcat’s or Netty’s) can cause thread pool exhaustion.
- In UI applications, blocking the UI thread (like in JavaFX or Swing) causes the interface to freeze.
- In microservices, blocking threads can reduce throughput and lead to degraded performance or timeouts.
Example:
String result = completableFuture.get(); // blocks until the result is available
If this is called in a REST controller or an event loop thread, it may severely impact scalability.
2. Potential for Deadlocks
Problem:
Blocking operations like get()
can introduce deadlocks, especially if you are trying to combine multiple futures or call get()
within an async callback.
Why it’s dangerous:
- If a thread holding a lock calls
get()
and waits for another task that needs the same lock, a deadlock can occur. - If you accidentally create circular dependencies between futures, the application can hang permanently.
Example:
CompletableFuture<String> cf1 = ...; CompletableFuture<String> cf2 = cf1.thenApply(data -> { return "Processed: " + data; }); String result = cf2.get(); // if cf1 is waiting for cf2, deadlock
Such indirect dependencies can create subtle and hard-to-debug deadlocks in production.
3. Unbounded Waiting
Problem:get()
waits forever by default if the future never completes.
Why it’s dangerous:
- If the asynchronous task never finishes (e.g., stuck network call, infinite loop, exception),
get()
will hang indefinitely. - Threads will accumulate and get blocked forever, potentially leading to OutOfMemoryError or thread pool exhaustion.
Safer alternative:
String result = completableFuture.get(5, TimeUnit.SECONDS);
Using a timeout protects your application from hanging forever and allows graceful error handling.
4. Poor Error Handling
Problem:
If the task fails, get()
throws an ExecutionException
, which wraps the actual exception thrown during computation.
Why it’s dangerous:
- Developers often forget to handle the wrapped exception properly.
- Catching the
ExecutionException
without unwrapping it can obscure the root cause.
Example:
try { String result = completableFuture.get(); } catch (ExecutionException e) { Throwable cause = e.getCause(); // important to retrieve real exception }
If you ignore e.getCause()
, you might miss meaningful error messages or stack traces.
5. Not Idiomatic for Asynchronous Programming
Problem:
Using get()
defeats the purpose of asynchronous programming.
Why it’s dangerous:
- It turns asynchronous code into synchronous, negating all benefits of using
CompletableFuture
. - If your architecture relies on non-blocking, event-driven execution (like in reactive programming), using
get()
introduces bottlenecks.
Better alternative:
completableFuture.thenAccept(result -> { // process result asynchronously });
This approach is non-blocking and allows better utilization of CPU and threads.
6. Uncaught InterruptedException
Problem:get()
can throw InterruptedException
, which many developers either ignore or mishandle.
Why it’s dangerous:
- Failing to properly handle thread interruption can lead to unpredictable behavior.
- Best practice is to restore the thread’s interrupted status if you catch
InterruptedException
.
Correct handling:
try { completableFuture.get(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); // preserve interrupt status }
Ignoring it can violate the contract of cooperative cancellation in Java concurrency.
Summary of Why get()
Can Be Dangerous in Production
Risk | Explanation |
---|---|
Blocks current thread | Wastes CPU and reduces concurrency |
Can lead to deadlocks | Especially in poorly chained or nested futures |
Unbounded waiting | If the task never completes, thread hangs forever |
Poor error transparency | Exceptions are wrapped and often mishandled |
Anti-pattern in async systems | Synchronous wait defeats async benefits |
Interrupts often mishandled | Can break thread interruption protocol |