Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE

1. Introduction

Future and Promise are tools used to handle asynchronous tasks, allowing one to execute operations without waiting for each step to complete. Although they both serve the same purpose, they exhibit key differences. In this tutorial, we’ll explore the differences between Future and Promise, scrutinizing their key characteristics, use cases, and distinctive features.

2. Understanding Future

Future acts as a container, awaiting the outcome of ongoing operations. Developers commonly employ Future to check the status of computations, retrieve results upon readiness, or gracefully wait until the operations conclude. Future often integrates with the Executor framework, providing a straightforward and efficient approach to handling asynchronous tasks.

2.1. Key Characteristics

Now, let’s explore some of Future‘s key characteristics:

  • Adopt a blocking design, which potentially leads to a wait until the asynchronous computation is complete.
  • Direct interaction with the ongoing computation is restricted, maintaining a straightforward approach.

2.2. Use Cases

Future excels in scenarios where the result of an asynchronous operation is predetermined and cannot be altered once the process begins.

Consider fetching a user’s profile information from a database or downloading a file from a remote server. Once initiated, these operations have a fixed outcome, such as the data retrieved or the file downloaded, and cannot be modified mid-process.

2.3. Using Future

To utilize Future, we can find them residing in the java.util.concurrent package. Let’s look at a code snippet demonstrating how to employ a Future for asynchronous task handling:

ExecutorService executorService = Executors.newSingleThreadExecutor();

Future<String> futureResult = executorService.submit(() -> {
    Thread.sleep(2000);
    return "Future Result";
});

while (!futureResult.isDone()) {
    System.out.println("Future task is still in progress...");
    Thread.sleep(500);
}

String resultFromFuture = futureResult.get();
System.out.println("Future Result: " + resultFromFuture);

executorService.shutdown();

And let’s check the output we get when we run the code:

Future task is still in progress...
Future task is still in progress...
Future task is still in progress...
Future task is still in progress...
Future Result: Future Result

In the code, the futureResult.get() method is a blocking call. This means that when the program reaches this line, it will wait until the asynchronous task submitted to the ExecutorService is complete before moving on.

3. Understanding Promise

In contrast, the concept of a Promise is not native to Java but is a versatile abstraction in other programming languages. A Promise acts as a proxy for a value that might not be known when the Promise is created. Unlike Future, Promise often provides a more interactive approach, allowing developers to influence the asynchronous computation even after its initiation.

3.1. Key Characteristics

Now, let’s explore some of Promise’s key characteristics:

  • Encapsulates a mutable state, permitting modification even after the asynchronous operation has begun, providing flexibility in handling dynamic scenarios
  • Employ a callback mechanism, allowing developers to attach callbacks executed upon completion, failure, or progress of the asynchronous operation

3.2. Use Cases

Promise is well-suited for scenarios where dynamic and interactive control over asynchronous operations is essential. Furthermore, Promise offers flexibility in modifying the ongoing computation even after initiation. A good example of this would be streaming real-time data in financial applications where the display content needs to adapt to live market changes.

Moreover, Promise is beneficial when dealing with asynchronous tasks that require conditional branching or modifications based on intermediate results. One possible use case is when we need to handle multiple asynchronous API calls where subsequent operations depend on the outcomes of previous ones.

3.3. Using Promise

Java might not have a dedicated Promise class that strictly adheres to the Promise specification as in JavaScript. However, we can achieve similar functionality using java.util.concurrent.CompletableFuture. CompletableFuture provides a versatile way to work with asynchronous tasks, sharing some characteristics with Promise. It’s important to note that they are not the same.

Let’s explore how to use CompletableFuture to achieve Promise-like behavior in Java:

ExecutorService executorService = Executors.newSingleThreadExecutor();
CompletableFuture<String> completableFutureResult = CompletableFuture.supplyAsync(() -> {
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return "CompletableFuture Result";
}, executorService);

completableFutureResult.thenAccept(result -> {
      System.out.println("Promise Result: " + result);
  })
  .exceptionally(throwable -> {
      System.err.println("Error occurred: " + throwable.getMessage());
      return null;
  });

System.out.println("Doing other tasks...");

executorService.shutdown();

When we run the code, we’ll see the output:

Doing other tasks...
Promise Result: CompletableFuture Result

We create a CompletableFuture named completableFutureResult. The supplyAsync() method is used to initiate an asynchronous computation. The provided lambda function represents the asynchronous task.

Next, we attach callbacks to the CompletableFuture using thenAccept() and exceptionally(). The thenAccept() callback handles the successful completion of the asynchronous task, similar to the resolution of a Promise, while exceptionally() handles any exceptions that might occur during the task, resembling the rejection of a Promise.

4. Key Differences

4.1. Control Flow

Once a Future‘s value is set, the control flow proceeds downstream, unaffected by subsequent events or changes. Meanwhile, Promise (or CompletableFuture) provides methods like thenCompose() and whenComplete() for conditional execution based on the final result or exceptions.

Let’s create an example of branching control flow using CompletableFuture:

CompletableFuture<Integer> firstTask = CompletableFuture.supplyAsync(() -> {
      return 1;
  })
  .thenApplyAsync(result -> {
      return result * 2;
  })
  .whenComplete((result, ex) -> {
      if (ex != null) {
          // handle error here
      }
  });

In the code, we use the thenApplyAsync() method to demonstrate the chaining of asynchronous tasks.

4.2. Error Handling

Both Future and Promise provide mechanisms for handling errors and exceptions. Future relies on exceptions thrown during the computation:

ExecutorService executorService = Executors.newSingleThreadExecutor();
Future<String> futureWithError = executorService.submit(() -> {
    throw new RuntimeException("An error occurred");
});

try {
    String result = futureWithError.get();
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
} finally {
    executorService.shutdown();
}

In CompletableFuture, the exceptionally() method is used to handle any exception that occurs during the asynchronous computation. If an exception occurs, it prints an error message and provides a fallback value:

CompletableFuture<String> promiseWithError = new CompletableFuture<>();
promiseWithError.completeExceptionally(new RuntimeException("An error occurred"));

promiseWithError.exceptionally(throwable -> {
    return "Fallback value";
});

4.3. Read-Write Access

Future provides a read-only view, allowing us to retrieve the result once the computation is complete:

Future<Integer> future = executor.submit(() -> 100);
// Cannot modify future.get() after completion

In contrast, CompletableFuture enables us not only to read the result but also to actively set values dynamically even after the asynchronous operation has started:

ExecutorService executorService = Executors.newSingleThreadExecutor();
CompletableFuture<Integer> totalPromise = CompletableFuture.supplyAsync(() -> {
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return 100;
}, executorService);

totalPromise.thenAccept(value -> System.out.println("Total $" + value ));
totalPromise.complete(10);

Initially, we set up the asynchronous task to return 100 as the result. However, we intervene and explicitly complete the task with the value 10 before it completes naturally. This flexibility highlights the write-capable nature of CompletableFuture, allowing us to dynamically update the result during asynchronous execution.

5. Conclusion

In this article, we’ve explored the distinction between Future and Promise. While both serve the purpose of handling asynchronous tasks, they differ significantly in their capabilities.

As always, the source code for the examples is available over on GitHub.

Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE
res – REST with Spring (eBook) (everywhere)
Comments are closed on this article!