Javascript Futures
 
Support Ukraine

Javascript Futures

Anyone writing anything in Javascript soon runs up against the fact that Javascript is a single-threaded language in a multi-threaded world.

1. Continuation Passing

The requirement that the browser remain responsive while still not going through state changes while the script runs - for example, the DOM can't change by itself while a script is running - results in Javascript having a continuation passing style. A continuation is simply "the rest of the program execution, encapsulated in a function object", and is best illustrated with a program that prints "Hello", waits one second, and then prints "World!".

To see the difference, we'll start off by showing how we would do this in a non-continuation-passing scenario:

console.log ("Hello");
wait (1000); // Made-up function that waits for the given
             // number of milliseconds.
console.log (" World!");

This is very clear - first we print one word, then we wait for 1000 milliseconds, then we print the second word. For a browser, the problem here is the one second sleep in the middle. While it runs, nothing can happen on the page. To avoid this we will wrap the third line into a function and use the setTimeout function:

console.log ("Hello");
var continuation = function () {
    console.log (" World!");
};
setTimeout (continuation, 1000);

The browser is now free to let the page go through any state changes needed. If we view the setTimeout as a special case of an event handler, the continuation passing style can be summarized as:

...do stuff...
var continuation = function () {
    ...things to do after the asynchronous call...
};
set continuation as event listener

The event can be the completion of a setTimeout, it can be a readyStateChange event of an XMLHttpRequest or a transitionend when dealing with CSS transitions.

2. Synchronous and Asynchronous

To discuss these processes we need to introduce the concepts of synchronous and asynchronous function calls. A synchronous call is the standard function call that every programmer knows - you call the function with the parameters you want, get the result back and move on to the next step of the computation:

var result = myObject.myFuction (myParameter);
var result2 = myObject.myOtherFunction (result);

In an asynchronous call, the result isn't returned using the normal value return mechanism. Instead, you set up a receiver that will, at some future point, receive the result:

myObject.addEventListener (
    "complete",
    function (result) { 
        ... 
    }
);
myObject.myFunction ();

Since we often think of programs in an imperative language such as Javascript as a sequence of "do this, then do this, then this", the asynchronous call takes time getting used to.

The property of "being called asynchronously" is contagious: If a function has an asynchronous call somewhere in its body, then it is very likely that the function itself must be called asynchronously. There are exceptions to this - for example, if we're not interested in the result of the asynchronous call - but in general, asynchronicity is contagious. This makes it vital that we are able to combine several asynchronous calls, an issue that we will return to later.

3. The Problem

Given that Javascript works, what's the problem? It's not just one problem, but several:

  • Lack of standards: As the end of the first section hinted at, there are many, many ways to actually do an asynchronous call in Javascript. Callbacks - such as the setTimeout example and event listeners on objects - such as the asynchronous call in the previous section are just two ways. If we include the plethora of different event names, making an asynchronous call is something that requires careful study of the API documentation and browser compatibility tables.

  • No ordering guarantees: Let's say that we have an object that performs a web page fetch. The API is simple, we pass a URL to fetch and a function that will receive the result:

    console.log ("About to call fetcher.fetch...");
    fetcher.fetch (
        "http://example.com/", 
        function receiver (doc) {
            console.log ("Got document!");
            ...do stuff with the fetched document...
        }
    );
    console.log ("Fetching document...");

    Typically this will result in the following console output:

    About to call fetcher.fetch...
    Fetching document...
    Got document!

    But what if the fetcher can satisfy the request from an internal cache? It could then call the receiver function in-line, result in the following order of execution:

    About to call fetcher.fetch...
    Got document!
    Fetching document...
  • No easy composability: This is a follow-on from the first point. With so many ways, and so few guarantees, having more than one asynchronous call running is difficult and requires ad-hoc solutions. For something that is so central to development in Javascript, asynchronous calls have received remarkably little attention in the standard library.

4. Futures

Futures solve all these problems. If a continuation is the caller wrapping up the rest of its execution in an object, a Future is the callee wrapping up a promise to yield a result in an object. Briefly, a future can be seen as an object with a single get() function (see §6. Future Implementation for full reference) that is used like this:

// 1. Do the asynchronous call
var future = myAsyncFunction (param);

// 2. Somehow we find out that the 
//    async function has completed

// 3. Retrieve the result
var result = future.get ();

Since we have no blocking wait functions in Javascript, we have to replace part (2) and (3). First, we define a Joiner as a function taking two arguments, a result and an optional Error: function Joiner (result, error) { ... }. Then, instead of a get(), we use a join(Joiner).

// 1. Do the asynchronous call
var future = myAsyncFunction (param);

// 2. Join to retrieve the result
future.join (function (result, error) {
    // Do stuff with result
});

4.1. Advantages

Futures solve the issues raised in §3. The Problem:

  • Standard: The greatest advantage is that we now have one way of handling asynchronous calls, not several. An asynchronous call returns a Future that is later join()ed.

  • Strict ordering: Since the client code must call join(), we have a strict ordering guarantee.

  • Composable: A future can easily be connected to another future. If an asynchronous function calls another asynchronous function, the calls can easily be joined:

    function myAsyncFunction () {
        // This is the future myAsyncFunction will return.
        var myFuture = new Future ();
        
        var otherFuture = otherAsyncFunction ();
        
        // Pass along result from otherAsyncFunction to 
        // myFuture.
        otherFuture.join (function (r,e) {
            if (e) {
                myFuture.error (e);
            } else {
                myFuture.complete (r);
            }
        });
        
        return myFuture;
    }

5. Await

It is interesting to note that a hypothetical dialect of Javascript with the keywords async, yield and await, used like this - with A(), B() and C() standing in for arbitrary blocks of (synchronous) code:

async function myFunction () {
    A();
    var future = asyncFunction ();
    B();
    await var result = future;
    C();
    yield result;
}

Can be transformed into the following standard Javascript with futures (error handling omitted):

function myFunction () {
    var _yielded = new Future ();
    A();
    var future = asyncFunction ();
    B();
    future.join (function (result) {
        C();
        _yielded.complete (result);
    });
    return _yielded;
}

That is, await var result = future is turned into a future.join (function (result) { ... }); with the function body being the rest of the function in which await is called; yield result is transformed into _yielded.complete (result), and async function means that we create a var _yielded = new Future() at the top of the function body.

6. Future Implementation

I've left out checks to ensure that the future isn't complete'd or error'd twice, as well as some other nice-but-not-need parts. For example, in reality we'd only expect one call to join, making it possible to not instance an Array of waiters unless we actually need to keep track of more than one waiter. However, putting all of this in would obscure the general functioning of a Future.

/**
 * Joins a future.
 * @name Future.Joiner
 * @function
 * @param result the result of the future
 * @param {Error} [error] the error, if any
 */

/**
 * Creates a new, uncomplete, Future.
 * @name Future
 * @class A future result.
 */
Future = function () {
    this.completed = false;
    this.error = null;
    this.result = null;
    this.isError = false;
    this.waiters = new Array ();
}

Future.prototype = {
    /**
     * Completes this future normally with the given result.
     *
     * @param r the result of the future
     */
    complete : function (r) {
        this.result = r;
        this.completed = true;
        this.notifyWaiting ();
    },
    
    /**
     * Returns true if the future is completed (normally or error)
     *
     * @type boolean
     * @returns true if the future has completed
     */
    isCompleted : function () {
        return this.completed;
    },
    
    /**
     * Returns true if the future has completed with errors.
     *
     * @type boolean
     * @returns true if the future has completed and has errors
     */
    hasError : function () {
        return this.completed && this.isError;
    },
    
    /**
     * Returns true if the future is completed normally
     *
     * @type boolean
     * @returns true if the future has completed without errors
     */
    hasResult : function () {
        return this.completed && !this.isError;
    },
    
    /**
     * Gets the result or throws the error.
     *
     * @type boolean
     * @returns the result
     * @throws Error if the future hasn't completed, or completed with errors
     */
    get : function () {
        if (!this.completed) {
            throw new Error ("Future is not completed.");
        }
        if (this.isError) {
            throw this.error;
        }
        return this.result;
    },
    
    /**
     * Returns the result without checking for completion or error.
     */
    getResult : function () {
        return this.result;
    },
    
    /**
     * Completes this future with an error.
     */
    error : function (e) {
        this.error = e;
        this.completed = true;
        this.isError = true;
        this.notifyWaiting ();
    },
    
    /**
     * Completes the future by running a function.
     */
    run : function (func, async) {
        if (async || Future.isAsync (func)) {
            func ().join (this.joiner ());
        } else {
            try {
                this.complete (func ());
            } catch (e) {
                this.error (e);
            }
        }
    },
    
    /**
     * Joins this future.
     *
     * @param {Future.Joiner} f the function to join with
     * @type Future
     * @returns this future
     */
    join : function (f) {
        if (this.completed) {
            this.notify (f);
        } else {
            this.waiters.push (f);
        }
        return this;
    },
    
    /**
     * Creates a joiner function for this future.
     *
     * @public
     * @type Future.Joiner
     */
    joiner : function () {
        var that = this;
        return function (r,e) {
            if (e) {
                that.error (e);
            } else {
                that.complete (r);
            }
        };
    },
    
    /**
     * Notifies all waiting joiners.
     * @private
     */
    notifyWaiting : function () {
        for (var i = 0; i < this.waiters.length; ++i) {
            this.notify (this.waiters[i]);
        }
        this.waiters = null;
    },
    
    /**
     * Notifies a single joiner.
     * @private
     */
    notify : function (f) {
        f (this.result, this.error);
    }
};

7. Further Reading

Promises/A[a] - A proposal on the CommonJS wiki that does almost what the Future implementation here does.

c# 5.0 - New C# await feature[b] - An explanation of the C# 5.0 await feature.

Task.ContinueWith(TResult)[c] - The C# equivalent of join in the C# Task framework (.Net framework 4).

2012-04-06, updated 2012-04-10