AlgoMaster Logo

CompletableFuture

22 min readUpdated December 16, 2025
Listen to this chapter
Unlock Audio

CompletableFuture

You call an external API. While waiting for the response, your thread sits idle. Wasted.

Traditional Futures force you to block. Call get() and your thread waits, doing nothing until the result arrives. With multiple async operations, you either block sequentially (slow) or manage callbacks manually (messy).

CompletableFuture solves this. It lets you chain operations, combine results, and handle errors, all without blocking. When one operation completes, the next starts automatically.

In this article, we'll explore:

  • Why traditional Futures fall short
  • How CompletableFuture enables non-blocking async code
  • Chaining, combining, and composing operations
  • Error handling strategies
  • Common patterns and pitfalls

If you're enjoying this newsletter and want to get even more value, consider becoming a [paid subscriber](https://blog.algomaster.io/subscribe).

As a paid subscriber, you'll unlock all premium articles and gain full access to all [premium courses](https://algomaster.io/newsletter/paid/resources) on [algomaster.io](https://algomaster.io).

Unlock Full Access

1. The Problem with Traditional Futures

A regular Future represents a pending result. But to get that result, you must block.

Problems with Blocking

Sequential blocking kills performance:

Combining results is awkward:

No built-in error handling:

2. What is CompletableFuture?

CompletableFuture is a Future that can be:

  • Completed manually (not just by executor)
  • Chained with callbacks that run when it completes
  • Combined with other CompletableFutures
  • Composed into pipelines of async operations

Non-Blocking Chaining

3. Creating CompletableFutures

From Async Computation

Completed Immediately

Manual Completion

4. Chaining Operations

CompletableFuture provides three main methods for chaining based on what you need from the result.

thenApply: Transform Result

Takes the result, returns a transformed value.

thenAccept: Consume Result

Takes the result, returns nothing (void).

thenRun: Run After Completion

Ignores the result, just runs after completion.

Summary

Scroll
Method
Input
Output
Use Case

thenApply

Result

New value

Transform data

thenAccept

Result

Void

Consume/process data

thenRun

Nothing

Void

Run side effect

5. Combining Multiple Futures

thenCombine: Combine Two Independent Results

Wait for two futures, combine their results.

thenCompose: Chain Dependent Futures

When one async operation depends on another's result (flatMap).

allOf: Wait for All

Wait for multiple futures to complete.

anyOf: First to Complete

Returns when any future completes.

6. Error Handling

CompletableFuture propagates exceptions through the chain. You can handle them at any point.

exceptionally: Recover from Errors

Provide a fallback value when an exception occurs.

handle: Handle Both Success and Failure

Process both the result and exception.

whenComplete: Side Effects on Completion

Run code when complete, but don't transform the result.

Comparison

Scroll
Method
Access Result
Access Exception
Can Transform
Can Recover

exceptionally

No

Yes

No

Yes

handle

Yes

Yes

Yes

Yes

whenComplete

Yes

Yes

No

No

Exception Propagation

Exceptions propagate through the chain until handled:

7. Async Variants

Each chaining method has an async variant that runs in a different thread.

Sync vs Async Execution

When to Use Async

Use Sync (thenApply)
Use Async (thenApplyAsync)

Lightweight transformation

CPU-intensive processing

Same thread is fine

Need parallel execution

Default behavior

Need specific executor

Async Methods

Sync
Async

thenApply

thenApplyAsync

thenAccept

thenAcceptAsync

thenRun

thenRunAsync

thenCombine

thenCombineAsync

thenCompose

thenComposeAsync

handle

handleAsync

whenComplete

whenCompleteAsync

8. Custom Executors

By default, async methods use the common ForkJoinPool. For production, use custom executors.

Why Custom Executors?

Common ForkJoinPool
Custom Executor

Good for CPU-bound tasks

Good for I/O-bound tasks

Shared across app

Isolated per use case

Can cause thread starvation

Predictable behavior

9. Getting the Result

When you finally need the value, you have options:

get() - Blocks, Throws Checked Exceptions

join() - Blocks, Throws Unchecked Exceptions

getNow() - Non-Blocking

Comparison

Scroll
Method
Blocking
Exception Type
Timeout

get()

Yes

Checked

Optional

join()

Yes

Unchecked

No

getNow(default)

No

Unchecked

N/A

10. Common Patterns

Pattern 1: Parallel Independent Calls

Pattern 2: Sequential Dependent Calls

Pattern 3: Timeout with Fallback

Pattern 4: First Successful Response

Pattern 5: Retry on Failure

Pattern 6: Batch Processing with allOf

11. Common Pitfalls

Pitfall 1: Blocking in Async Chain

Pitfall 2: Lost Exceptions

Pitfall 3: Using Wrong Thread Pool

Pitfall 4: Creating Too Many Futures

Pitfall 5: Ignoring Return Values

Pitfall 6: Nested thenApply Instead of thenCompose

12. CompletableFuture vs Traditional Approaches

Scroll
Approach
Blocking
Composability
Error Handling
Readability

Future + get()

Yes

Poor

Manual

Medium

Callbacks

No

Poor (nesting)

Scattered

Poor

CompletableFuture

Optional

Excellent

Built-in

Good

13. Summary

CompletableFuture enables non-blocking, composable asynchronous programming.

Method Reference

Need
Method

Transform result

thenApply

Consume result

thenAccept

Run after completion

thenRun

Chain dependent async

thenCompose

Combine two results

thenCombine

Wait for all

allOf

Wait for first

anyOf

Handle error

exceptionally

Handle both

handle

Side effect

whenComplete

Best Practices

  1. Use thenCompose for dependent async calls, not thenApply
  2. Always handle exceptions with exceptionally or handle
  3. Use custom executors for I/O-bound operations
  4. Avoid blocking (get/join) inside the chain
  5. Chain methods properly, don't ignore return values
  6. Limit concurrency when processing large collections
  7. Use orTimeout for external service calls
  8. Prefer join() over get() for cleaner exception handling

CompletableFuture transforms callback spaghetti into clean, composable pipelines. Master it, and you can write efficient async code that's also readable and maintainable.

Thank you for reading!

If you found it valuable, hit a like and consider subscribing for more such content every week.

If you have any questions or suggestions, leave a comment.

This post is public so feel free to share it.

Share

P.S. If you're enjoying this newsletter and want to get even more value, consider becoming a [paid subscriber](https://blog.algomaster.io/subscribe).

As a paid subscriber, you'll unlock all premium articles and gain full access to all [premium courses](https://algomaster.io/newsletter/paid/resources) on [algomaster.io](https://algomaster.io).

Unlock Full Access

There are [group discounts](https://blog.algomaster.io/subscribe?group=true), [gift options](https://blog.algomaster.io/subscribe?gift=true), and [referral bonuses](https://blog.algomaster.io/leaderboard) available.

Checkout my [Youtube channel](https://www.youtube.com/@ashishps_1/videos) for more in-depth content.

Follow me on [LinkedIn](https://www.linkedin.com/in/ashishps1/) and [X](https://twitter.com/ashishps_1) to stay updated.

Checkout my [GitHub repositories](https://github.com/ashishps1) for free interview preparation resources.

I hope you have a lovely day!

See you soon, Ashish

References

Images Needed

[IMAGE 1: Blocking vs Non-Blocking] <!-- Shows thread waiting vs thread continuing with callbacks -->

[IMAGE 2: CompletableFuture Pipeline] <!-- Shows stages connected in a pipeline -->

[IMAGE 3: thenApply vs thenCompose] <!-- Visual showing flat vs nested futures -->

[IMAGE 4: allOf vs anyOf] <!-- Shows waiting for all vs waiting for first -->

[IMAGE 5: Exception Propagation] <!-- Shows exception flowing through chain until handled -->

[IMAGE 6: Parallel Independent Calls Pattern] <!-- Shows multiple calls running in parallel, then combining -->