In the previous section, we covered the basics of CompletableFuture
: chaining, combining, exception handling, and real-world use cases. Now, let’s go one step further and look at advanced features that make it production-ready:
8. Setting Timeouts (orTimeout
& completeOnTimeout
)
Sometimes tasks hang due to slow APIs or network issues. Instead of blocking forever, CompletableFuture
lets you set timeouts.
orTimeout()
→ fail after timeout
import java.util.concurrent.*;
public class CompletableFutureTimeout {
public static void main(String[] args) {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
try { Thread.sleep(3000); } catch (InterruptedException e) {}
return "Completed!";
}).orTimeout(2, TimeUnit.SECONDS);
try {
System.out.println(future.get());
} catch (Exception e) {
System.out.println("Timeout! " + e.getMessage());
}
}
}
👉 After 2 seconds, the future throws a TimeoutException
.
completeOnTimeout()
→ return default value if timeout
public class CompletableFutureCompleteOnTimeout {
public static void main(String[] args) throws Exception {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
try { Thread.sleep(3000); } catch (InterruptedException e) {}
return "Data loaded!";
}).completeOnTimeout("Fallback value", 2, TimeUnit.SECONDS);
System.out.println(future.get()); // prints "Fallback value"
}
}
9. Cancelling a Task
If a task is no longer needed, you can cancel it.
import java.util.concurrent.*;
public class CompletableFutureCancel {
public static void main(String[] args) throws Exception {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(5000);
return "Finished!";
} catch (InterruptedException e) {
return "Interrupted!";
}
});
Thread.sleep(1000);
boolean cancelled = future.cancel(true); // tries to stop execution
System.out.println("Cancelled: " + cancelled);
}
}
10. Using Custom ExecutorService
By default, CompletableFuture
uses the ForkJoinPool.commonPool(). For better control, especially in production, you should provide a custom thread pool.
import java.util.concurrent.*;
public class CompletableFutureCustomExecutor {
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(4);
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
return "Running in custom executor: " + Thread.currentThread().getName();
}, executor);
System.out.println(future.get());
executor.shutdown();
}
}
📌 Why use a custom executor?
- Prevents blocking the common pool with long tasks.
- Lets you tune thread counts for CPU-bound vs I/O-bound workloads.
11. Any-of Pattern (anyOf
)
When you want the fastest response from multiple tasks.
import java.util.concurrent.*;
public class CompletableFutureAnyOf {
public static void main(String[] args) throws Exception {
CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> {
sleep(2000);
return "Result from service 1";
});
CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> {
sleep(1000);
return "Result from service 2";
});
CompletableFuture<Object> fastest = CompletableFuture.anyOf(f1, f2);
System.out.println("Winner: " + fastest.get());
}
private static void sleep(int ms) {
try { Thread.sleep(ms); } catch (InterruptedException e) {}
}
}
👉 Output:
Winner: Result from service 2
12. Pipelining with Dependent Async Calls
Imagine fetching user details, then fetching their orders.
public class CompletableFuturePipeline {
public static void main(String[] args) throws Exception {
CompletableFuture<String> userFuture = CompletableFuture.supplyAsync(() -> {
sleep(1000);
return "User123";
});
CompletableFuture<String> ordersFuture = userFuture.thenCompose(user ->
CompletableFuture.supplyAsync(() -> {
sleep(1500);
return "Orders for " + user;
})
);
System.out.println(ordersFuture.get());
}
private static void sleep(int ms) {
try { Thread.sleep(ms); } catch (InterruptedException e) {}
}
}
👉 thenCompose()
is used when one async task depends on another.
🔑 Key Takeaways
- Timeouts prevent tasks from blocking forever.
- Cancellation helps stop unnecessary tasks.
- Custom executors give better performance control.
- anyOf/allOf enable powerful parallel task handling.
- thenCompose is the go-to for dependent async calls.
✅ Final Thoughts
CompletableFuture
is not just about async execution—it’s a framework for orchestrating tasks:
- Run tasks in parallel
- Chain dependent pipelines
- Handle timeouts, retries, errors
- Customize execution with executors
When used correctly, it helps build high-performance, non-blocking, resilient applications in Java.
Would you like me to also include a full mini-project example (like fetching from 3 services with timeout, fallback, and parallel execution) so you can see how all these features work together in real-world code?
Top comments (0)