Backend Development
A Node developer’s perspective on Java promises
Transitioning from a callback-based NodeJS world into Java, I was surprised to find similar support for asynchronous computation in Java 8. Java is known for being a blocking language, so methods for handling long-running actions asynchronously are not necessarily a given. How well does Java’s promise equivalent fare for a developer used to Node? Let’s walk through a pizza-serving app to see how they stack up.
In order for Java to support asynchronous computation, it first needs to introduce a way for the synchronous and asynchronous method calls to talk with one another. This is done via the Future interface, which has a single get
method that returns the computed result. The addition of this interface abstracts away the complexity of knowing when an action completes and allows Java to treat the two types identically. Consider the following example:
ExecutorService executor = Executors.newCachedThreadPool(); Future<Pizza> future = executor.submit(new Callable() { public Pizza call() { return cookPizza(); } }); ... try { Pizza pizza = future.get(); // grab cooked pizza eat(pizza); } catch (final InterruptedException | ExecutionException e) { // not cooked }
In JavaScript, on the other hand...
cookPizza().then(function(pizza) { eat(pizza); });
Note the slight implementation difference here. The Future interface follows a pull model where an explicit get
call needs to occur in order to get the result. This differs from JavaScript callbacks as its push-based model handles the execution implicitly when the promise completes. I found the push-based model much more favorable because if you forget the explicit call, the asynchronous computation never kicks off!
Using Java Promises
While the Future interface has been available in Java since release 1.5, full promise support wasn’t introduced until Java 8 because Future interfaces cannot be chained together. Chaining is a way to join multiple steps that depend on the results of the long-running computation, which is the most common use case for asynchronous computation. The newly introduced CompletableFuture class provides this crucial functionality and allows Java’s implementation to parallel NodeJS.
CompletableFuture .supplyAsync(this::cookPizza) .thenAccept(this::eat) .join(); // starts execution
Here, we have a simple Java promise chain that eats the pizza after cooking it. Note there are multiple method calls being chained to the CompletableFuture. This is needed because Java is a strongly-typed language, meaning the type of each parameter is checked at compile-time. Java 8 functional interfaces are used for the CompletableFuture to differentiate between the various method inputs. While we encourage diving into those interfaces more deeply, the explanation of the different interfaces provided is out of scope for this post. In lieu of that, here’s a quick summary of the ones I will use:
Method | Input | Output |
supplyAsync | Nothing | Object |
thenAccept | Object | Nothing |
thenApply | Object | Object |
On the other hand, these various method calls are not needed when using JavaScript because it's a loosely-typed language that doesn't check the input types, yielding only a single then
call for chaining. While this does allow for simpler code upfront, you need to be careful with the objects passed between promises. The object types are not checked, which means in JavaScript, the cookPizza
function could return something unexpected, like sardines, that would be eaten unless explicit checking is added that a pizza object is actually returned (although sardines would make for quite a dinner!).
cookPizza().then(function(pizza) { if (isPizza(pizza)) { eat(pizza); } else { console.log('trickster tried feeding me something other than a pizza!'); } });
Chaining More Actions
Above, we chained a single long-running asynchronous call to a dependent synchronous call. However, typical promise usage has multiple long-running calls that depend on one another. Luckily this is a simple extension in both implementations, since each method returns output that can be subsequently chained or (in Java) executed. Let’s see an example of this in action.
CompletableFuture .supplyAsync(this::cookPizza) .thenApply(this::slicePizza) .thenAccept(this::eat) .join();
The equivalent in JavaScript is as follows:
cookPizza().then(function(pizza) { slicePizza(pizza).then(function(pizza) { eat(pizza); }); });
You can see here that the Java implementation allows a more streamlined way to chain actions together at the top level, rather than chaining by nesting, like in JavaScript. This is possible because each of the Java method calls returns a CompletableFuture. Due to JavaScript’s then
not returning another Promise by default, we have to explicitly return a new Promise in order to continue chaining at the top level.
cookPizza() .then(function(cookedPizza) { return new Promise(function(resolve, reject) { setTimeout(function() { cookedPizza.slices = 8; resolve(cookedPizza); }); }); }) .then(function(slicedPizza) { eat(slicedPizza); });
Exception Handling
But what happens when the cooking goes awry? To deal with that scenario, we will chain an exception handler in our example so we don’t eat a burned pizza. Java provides the exceptionally
function, which can be chained directly after supplying the long-running action.
CompletableFuture .supplyAsync(this::cookPizza) .exceptionally((overcooked) -> { // burnt }) .thenAccept(this::eat) .join();
The equivalent implementation in JavaScript uses the familiar catch
terminology, from try/catch external exception handling, in a chaining format.
cookPizza() .then(function(pizza) { eat(pizza); }) .catch(function(overcooked) { // burnt });
Note here that the exception-handlers for both implementations can be added after each promise in the chain or at the end to handle the entire group. In either case, the code will fall through to the closest exception-handling definition when things go wrong.
Parallelizing Independent Actions
Now let’s get into some more complex examples. Before serving the pizza, you probably want to set the table. Since this long-running action is independent from the cookPizza
function, we can execute them in parallel using their “resolve all” methods.
Promise.all([setTable(), cookPizza()]) .then(function(result) { // result is an array following each index as passed in let pizza = result[1]; eat(pizza); });
Side note: One thing to mention here is the JavaScript examples are all implemented using native ES6. For more complex Promise manipulation, there are a variety of libraries such as Bluebird and Q that can be used.
We hit a blocker when trying to implement the same using Java.
CompletableFuture .allOf(new CompletableFuture[] { CompletableFuture.supplyAsync(this::setTable), CompletableFuture.supplyAsync(this::cookPizza) }) .thenAccept((empty) -> { // eat needs something to consume! eat(???); }) .join(); … private Pizza cookPizza() { return new Pizza("cooked"); }
This happens because the allOf
implementation from Java returns a CompletableFuture of type Void, meaning access to the cooked pizza is lost. To work around this restriction while maintaining the parallelism of the call, we keep the desired result in a globally-accessible variable that can be read by the thenAccept
portion of the call.
private Pizza pizza; … CompletableFuture .allOf(new CompletableFuture[] { CompletableFuture.supplyAsync(this::setTable), CompletableFuture.supplyAsync(this::cookPizza) }) .thenAccept((empty) -> { // some sanity checking to make sure pizza isn't burnt if (pizza.isCooked) { eat(pizza); } }) .join(); … private Pizza cookPizza() { pizza = new Pizza("cooked"); return pizza; }
Grouping Actions Together
While this work around does yield the expected result, using a globally-modifiable variable is less than ideal. Since we only have two promises in the list in this case, we can use thenComposeAsync
, as long as we’re willing to give up the benefits of parallelism by specifying an order of completion.
CompletableFuture .supplyAsync(this::setTable) .thenComposeAsync((table) -> { return CompletableFuture.supplyAsync(this::cookPizza); }) .thenAccept((pizza) -> { eat(pizza); }) .join();
Since we have effectively removed the parallelization from that example, let’s use this function for a more fitting use case: adding toppings to the pizza. This is where the thenComposeAsync
function really shines since multiple actions can be invoked with only a single result. At the “add toppings” stage, we don’t care which of the topping additions fails, since a Hawaiian pizza isn’t complete without both the pineapple and ham toppings. Using the thenComposeAsync
, the separate add-topping actions can be executed in a single step before the pizza is eaten.
CompletableFuture .supplyAsync(this::cookPizza) .thenComposeAsync((pizza) -> { // these are synchronous calls reliant on the long-running computation pizza.addTopping("pineapple"); pizza.addTopping("ham"); // return another CompletableFuture for chaining return CompletableFuture.supplyAsync(() -> { return pizza; }); }) .thenAccept((pizza) -> { eat(pizza); }) .join();
Unfortunately, there is no JavaScript equivalent to the thenComposeAsync
function to allow for grouping together actions into a single success/fail step.
Summary
While this is by no means a comprehensive comparison, Java Promises were a nice surprise for me coming from a callback-based NodeJS mindset. Both implementations of promises have their quirks, as Java could benefit from a version of Promise.all
to return the results of each array element and JavaScript could benefit from a thenComposeAsync
equivalent. However, these are nitpicks about two implementations that achieve the goal of maximizing the non-blocking portion of execution. As a recent addition to the standard library Java’s promise interface does feel less connected than in JavaScript, but this feature is a step in the right direction in an increasingly web-based callback world.