When.java: a Promises/A+ implementation
Promises and Flow of Control
Using Promises/A+ is a way to allow the programmer to compose asynchronous programs in a fashion similar to how synchronous programs are composed. A synchronous function can return values/throw exceptions; A Promise can become fulfilled with values/rejected with reasons. These properties determines the flow of control.
When a synchronous function throws an exception, the line-by-line flow of control is interrupted and transferred to a block of code that has declared itself capable of handling such exceptions: try/catch
. From that point on, the line-by-line execution of the program is resumed, until another exception occurs or the end of the program is reached.
In the asynchronous world, Promises help to emulate this compositional style. A Promise that implements the Promises/A+ specification implements the method then
, which lets the programmer declare callback functions to execute immediately after the Promise has become fulfilled
or rejected
. Furthermore, a call to then
generates a new Promise, whose state is bound to the return value of the callback functions. It is thus possible to create chains of Promises, where asynchronous calls are executed in a deterministic order according to certain rules:
-
if a Promise is
fulfilled
, control flow propagates to the nextonFulfilled
function in the chain. -
if a Promise is
rejected
, control flow propagates to the nextonRejected
function in the chain.
Thus, an onRejected
callback function in a chain of Promises assumes responsibility for any un-handled rejection that might have occurred up to that point, just as a catch
clause takes care of any exception that occurs within the corresponding try
block.
Let’s say we call then
on a Promise X, thus creating a new Promise Y. Let’s then say at a later time the state of Promise A transitions to an end state (fulfilled
or rejected
), which triggers a callback function execution. The state of Promise Y then
-
is bound to the return value, if the function call returns successfully
-
transitions to
rejected
, if the function call throws an exception
As pointed out in this blog post, it is an important detail of the Promises/A+ specification that a call to then
generates a new Promise. This is what enables Promises to work as an analogue to the try/catch control flow of synchronous programming. Quoting Domenic Denicola:
In other words,
then
is not a mechanism for attaching callbacks to an aggregate collection. It’s a mechanism for applying a transformation to a promise, and yielding a new promise from that transformation.
When.java
When.java is an Java implementation of the Promises/A+ specification, by ef-labs.
In When.java, the notion of a Promise is represented by the Promise<T>
interface, where type T
is the type of the promised value. Promise<T>
extends the Thenable<T>
interface, which has a single method with a pretty scary looking signature:
<U> Promise<U> then(
Function<T, ? extends Thenable<U>> onFulfilled,
Function<Throwable, ? extends Thenable<U>> onRejected
);
Note how the return type Thenable<U>
of the callback functions define the type Promise<U>
that is created by a call to then
. The output from a call to then
is thus itself a Promise, governed by the callback functions of the source Promise, as defined by the Promises/A+ spec.
then
is actually all there is to it, but to allow more readable code, When.javas’ Promise<T>
also defines a number of convenience methods, most importantly:
Method | Equal to |
---|---|
then(Function<...> onFulfilled) |
then(Function<...> onFulfilled, null) |
otherwise(Function<...> onRejected) |
then(null, Function<...> onRejected) |
The typical callback chain thus looks something like this:
Promise<String> promise1 = ...;
promise1.then(string -> {
Promise<Integer> promise2 = ...;
return promise2;
}).then(integer -> {
System.out.println("Result: " + integer);
return null;
}).otherwise(exception -> {
exception.printStackTrace();
return null;
});
where the last otherwise
clause will take care of any exception that occurs within the chain. If both Promises is eventually reaches an end state, the chain will evaluate to a fulfilled Promise with value null
.
TIP: In languages less strongly typed than Java, one might allow the callback functions to return either a Promise or a value. In Java this is of course not possible, but as a convenience, returning
null
from a callback function is effectively the same as returning afulfilled
Promise with fulfillment valuenull
.
Danger: silent exceptions
An uncaught exception that occurs on the main thread in a Java program usually leaves a loud and clear stack trace in stdout. To the contrary, a uncaught rejection that happens in a chain of When.java Promises is never logged by the framework, but just silently discarded.
Promise<String> promise = ...;
promise.then(s -> {
throw new NullPointerException("Oops!"); // this is not logged
});
The programmer needs to be extra careful and make sure that no unexpected errors occur, or add a otherwise
at the end of the chain that makes sure to log un-handled errors.
Danger: implicit return types
Note how the return type Thenable<U>
of the callback functions define the type Promise<U>
that is returned from a call to then
; i.e. the return type is defined by the arguments. This follows from the Promises/A+ specification and Javas’ type safety. The strong typing can lead to a false sense of security regarding types, but it is quite possible to trick the framework into a ClassCastException
:
Promise<String> promise = deferred.getPromise();
promise.then(string -> {
return when.resolve(new Object());
}).otherwise(exception -> {
return when.resolve("");
}).then(string -> { // ClassCastException occurs here
return when.resolve("");
}).otherwise(exception -> {
exception.printStackTrace();
return null;
});
The above example may be contrived, and can easily be avoided. Still, it might be good to be aware of this, if you are building more complex chains of Promises. A ClassCastException
is not really expected here since there are no explicit casts, and combined with the “silent exception” behavior described above, it could create some nasty bugs.
Unit testing Promise chains with Mockito
Suppose that you are working with an interface, designed, let’s say, for an asynchronous event bus, which defines a single method: you send in a String
message, and immediately get back a Promise<String>
that eventually should resolve to a String
response, or become rejected with some exception.
interface EventBus {
Promise<String> send(String message);
}
Suppose that you build a complex chain of calls to this method, sending messages back and forth and reacting to the responses in various ways:
public Promise<String> doSomethingAsync(EventBus bus, When when) {
Promise<String> result = bus.send("First message").then(response -> {
// do something with the response
return bus.send("Next message");
}).then(response -> {
if (response.equals("error")) {
throw new RuntimeException("Catastrophe!");
}
return bus.send("Final message");
}).then(response -> {
return when.resolve("Success!");
}).otherwise(exception -> {
// handle the error
return when.reject(exception);
});
return result;
}
You might want to write a simple unit test, to see what happens if the first call to send
returns a sane response, but the second response is corrupt in some way, or an error message. How would you do that?
Here’s a suggestion. First, define a helper class that lets you enqueue Promises, and then resolve or reject them one by one, in the order that they were enqueued:
public class PromiseQueue {
private final LinkedList<Deferred<?>> queue = new LinkedList<>();
<T> Promise<T> enqueue(Deferred<T> deferred) {
queue.addLast(deferred);
return deferred.getPromise();
}
<T> void resolveNext(T response) {
Deferred<T> deferred = (Deferred<T>) queue.removeFirst();
deferred.resolve(response);
}
void rejectNext(Throwable throwable) {
queue.removeFirst().reject(throwable);
}
}
Then, with the help of PromiseQueue
, mock the EventBus
class with Mockito:
EventBus buildMock(PromiseQueue queue) {
When when = WhenFactory.createSync();
EventBus eventBus = Mockito.mock(EventBus.class);
Mockito.when(eventBus.send(Mockito.anyString()))
.thenAnswer(invocation -> queue.enqueue(when.defer()));
return eventBus;
}
Now you may design simple self-contained unit tests, like this one:
@Test
public void test() {
PromiseQueue queue = new PromiseQueue();
EventBus bus = buildMock(queue);
Promise<String> result = doSomethingAsync(bus);
queue.resolveNext("this went well");
queue.resolveNext("error");
Assert.assertEquals(HandlerState.REJECTED, result.inspect().getState());
Assert.assertEquals("Catastrophe!", result.inspect().getReason().getMessage());
}
Simma lugnt.