Java
Promises
Asynchronous Programming
JavaScript
Software Development

Concept of promises in Java

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Java does not use the word "Promise" as its primary standard-library abstraction, but the concept exists in the form of CompletableFuture. A promise represents a value that may not be available yet but will eventually resolve successfully or fail. Once you understand that idea, asynchronous Java code becomes much easier to structure without blocking threads everywhere.

What a Promise Means in Practice

A promise is a handle to future completion. Instead of waiting immediately for a result, you describe what should happen when the result arrives.

In older Java concurrency code, Future gave only a limited version of that idea:

  • start a task
  • wait with get()
  • maybe check completion status

That often led to blocking code and awkward orchestration. CompletableFuture improves this by supporting continuation, composition, and error handling.

Simple example:

java
1import java.util.concurrent.CompletableFuture;
2
3public class Main {
4    public static void main(String[] args) {
5        CompletableFuture<String> future =
6                CompletableFuture.supplyAsync(() -> "hello");
7
8        future.thenAccept(System.out::println).join();
9    }
10}

The important idea is that the value is not needed immediately. The program registers a follow-up action that runs when the computation completes.

CompletableFuture Is Java's Promise-Like API

The closest built-in Java equivalent to JavaScript promises is CompletableFuture. It supports:

  • asynchronous creation
  • transformation of results
  • chaining dependent work
  • combining independent tasks
  • recovery from failure

If a task returns a result, use supplyAsync:

java
1CompletableFuture<Integer> future =
2        CompletableFuture.supplyAsync(() -> 21)
3                .thenApply(x -> x * 2);
4
5System.out.println(future.join());

If a task returns no value, use runAsync:

java
1CompletableFuture<Void> future =
2        CompletableFuture.runAsync(() -> System.out.println("background work"));
3
4future.join();

This is the same mental model as a promise chain: start asynchronously, then describe the next step.

Compose Async Work Instead of Blocking

The biggest benefit of promise-style programming is composition. Suppose one async call depends on the output of another:

java
1import java.util.concurrent.CompletableFuture;
2
3public class Main {
4    static CompletableFuture<String> fetchUser() {
5        return CompletableFuture.supplyAsync(() -> "alice");
6    }
7
8    static CompletableFuture<String> fetchProfile(String user) {
9        return CompletableFuture.supplyAsync(() -> "profile-for-" + user);
10    }
11
12    public static void main(String[] args) {
13        CompletableFuture<String> result =
14                fetchUser().thenCompose(Main::fetchProfile);
15
16        System.out.println(result.join());
17    }
18}

thenCompose is the promise-style flattening operator. Use it when the next step itself returns another CompletableFuture.

If you only transform a value synchronously, use thenApply instead:

java
CompletableFuture<Integer> result =
        CompletableFuture.supplyAsync(() -> 10)
                .thenApply(x -> x + 5);

That distinction is central:

  • 'thenApply for synchronous mapping'
  • 'thenCompose for async chaining'

Combine Independent Async Tasks

If two operations can run in parallel, combine them without blocking one on the other:

java
1import java.util.concurrent.CompletableFuture;
2
3public class Main {
4    public static void main(String[] args) {
5        CompletableFuture<Integer> a =
6                CompletableFuture.supplyAsync(() -> 20);
7
8        CompletableFuture<Integer> b =
9                CompletableFuture.supplyAsync(() -> 22);
10
11        CompletableFuture<Integer> sum =
12                a.thenCombine(b, Integer::sum);
13
14        System.out.println(sum.join());
15    }
16}

This is where promise-style APIs become much cleaner than manual thread coordination.

Handle Failure Explicitly

Promises are not just about successful completion. They also need a failure path.

java
1import java.util.concurrent.CompletableFuture;
2
3public class Main {
4    public static void main(String[] args) {
5        CompletableFuture<String> future =
6                CompletableFuture.supplyAsync(() -> {
7                    throw new RuntimeException("boom");
8                }).exceptionally(ex -> "fallback");
9
10        System.out.println(future.join());
11    }
12}

If you do not handle errors intentionally, async code tends to fail far away from where it was created, which makes production debugging harder.

Promises Are Not Automatic Performance

Using CompletableFuture does not automatically make code faster. If the underlying work is blocking I/O, the program still needs the right executor strategy and backpressure. Promise-style APIs improve structure and non-blocking composition, but they do not remove the cost of the real work being performed.

That is why high-quality async Java code pays attention to:

  • which executor runs the task
  • whether the underlying work blocks
  • how many tasks are started at once
  • how failures and timeouts propagate

Common Pitfalls

  • Treating CompletableFuture as if it were useful only when followed immediately by join().
  • Using thenApply when the mapping function actually returns another CompletableFuture.
  • Forgetting to handle failures and letting async exceptions surface late and unclearly.
  • Assuming promise-style code is automatically faster without considering executors and blocking work.
  • Starting dependent tasks sequentially when they could have been combined independently.

Summary

  • In Java, the promise concept is usually expressed with CompletableFuture.
  • A promise represents a result that will be available later, or fail.
  • Use thenApply for synchronous mapping and thenCompose for async chaining.
  • Combine independent tasks with operators such as thenCombine.
  • Promise-style APIs improve async structure, but real performance still depends on the actual workload and executor setup.

Course illustration
Course illustration

All Rights Reserved.