JavaScript Promises

March 9, 2016 by JavaScript   ES6  

A Promise acts like a placeholder for an asynchronous operation that hasn't completed yet, its aim is to give us back the things we "lost" (or never had for that matter) when writing asynchronous code opposed to synchronous code.

"Lost" in regards to logical flow (code structure) and language fundamentals, e.g functional composition (return) and error bubbling (throw)

Have a look at the following run-of-the-mill synchronous snippet:

var args = [];

function one() {
    return 1;
}

function two() {
    return 2;
}

function three() {
    return 3;
}
args.push(one());
args.push(two());
args.push(three());
console.log(args);


It is quite easy to follow the logical flow of the preceding snippet, easy to read, nothing awfully special about it (a well trained parrot would be able to maintain it).

Now look at the snippet rewritten to execute asynchronously using the old classic JavaScript way of writing callbacks.

function one(_return) {
    setTimeout(function() {
        _return(1);
    }, 200);
}

function two(_return) {
    setTimeout(function() {
        _return(2);
    }, 150);
}

function three(_return) {
    setTimeout(function() {
        _return(3);
    }, 100);
}


The first thing to understand about asynchronous code is that it is not always possible to predict when operations will be completed, e.g. AJAX calls (network issues, server bottlenecks), animation etc.

For that reason I purposely assigned different timeout values in the snippet above (to simulate this reality)

We end up with something like this:

one(function(arg) {
    args.push(arg);
    two(function(arg) {
        args.push(arg);
        three(function(arg) {
            args.push(arg);
            console.log(args);
        });
    });
});

This is what is known as the pyramid of doom, difficult to read / maintain, error prone and with various scope problems, a far cry from the simplicity of the original synchronous snippet, nesting of any kind should generally be avoided if possible.

Alien slaves built the pyramids

It even gets worse as soon as we introduce exception handling and error bubbling into the equation.

function two(_return, _failed) {
    setTimeout(function() {
        try {
            throw new Error('Oh no!');
            _return(2);
        } catch (e) {
            _failed(e);
        }
    }, 150);
}
one(function(arg) {
    args.push(arg);
    two(function(arg) {
        args.push(arg);
        three(function(arg) {
            args.push(arg);
            console.log(args);
        });
    }, function(error) {
        console.log(error);
    });
});


Ok, so lets clean this up a bit using promises.

Initially I was going to do a little write up on JQuery's broken promises (see what I did there?) but decided to give it a skip, there is a link further down that goes into great detail about it (even the 2.x branch is broken).

Instead I am going to use a library called Q (which is fully Promise/A+ Compliant).

Q Star Trek agrees

Observe the following snippet:

function asyncOne() {
    return Q.promise(function(resolve) {
        setTimeout(function() {
            console.log(1);
            resolve(1);
        }, 200);
    });
}

function asyncTwo() {
    return Q.promise(function(resolve) {
        setTimeout(function() {
            console.log(2);
            resolve(2);
        }, 150);
    });
}

function asyncThree() {
    return Q.promise(function(resolve) {
        setTimeout(function() {
            console.log(3);
            resolve(3);
        }, 100);
    });
}


Like seen in the snippet above, every function returns a promise, this allows us to do some chaining.

var args = [];
var addResult = function(arg) {
    args.push(arg);
};
asyncOne()
.then(addResult)
.then(asyncTwo)
.then(addResult)
.then(asyncThree)
.then(addResult).then(function() {
    console.log(args);
});

Now this is a lot more readable and dare I say elegant (no), one can easily follow the flow of code.

But what about exceptions?

function asyncTwo() {
    return Q.promise(function(resolve, reject) {         // throw new Error('Oh no!'); Will return promise 
        setTimeout(function() {             // throw over here will fail since it is out of process,             // therefore can't return a promise.
            reject('Oh no!');
            console.log(2);
            resolve(2);
        }, 150);
    });
}
var args = [];
var addResult = function(arg) {
    args.push(arg);
};
asyncOne()
  .then(addResult)
  .then(asyncTwo)
  .then(addResult)
  .then(asyncThree)
  .then(addResult)
  .then(function() {
    console.log(args);
}).catch(function(error) {
    console.log(error);
});

Properly implemented functions will / must always return a promise, even exceptions need to return promises, thereby maintaining our compositional chain.

Promises are either fulfilled (resolved) or rejected (by an unhandled exception or user code).

Up to this point all the examples you saw in this post used "sequential" promises, which basically means you've got promises waiting for other promises to finish before they do their thing. Obviously in certain scenarios that would be the preferred method, but what about tasks that can run independently?

You will notice that the example promises will run about 450ms to complete, note that even though we need our numbers in a sequential order, it doesn't mean that the code needs to run sequentially, the promises (in this context) can run parallel (down to 200ms) - the results will be returned in the order we requested them.

Q.all([asyncOne(), asyncTwo(), asyncThree()]).then(function(results) {
    console.log(results);
});

ECMAScript 6 (ES6)

Promises is also natively available to JavaScript for browsers that implement the ES6 specification so take care if you're planning to use the native implementation.

Like seen in the snippet below, the methods are identical to the Q implementation (thereby making future migration to ES6 promises quite easy).

function asyncOne() {
    return new Promise(function(resolve) {
        setTimeout(function() {
            console.log(1);
            resolve(1);
        }, 200);
    });
}

function asyncTwo() {
    return new Promise(function(resolve) {
        setTimeout(function() {
            console.log(2);
            resolve(2);
        }, 150);
    });
}

function asyncThree() {
    return new Promise(function(resolve) {
        setTimeout(function() {
            console.log(3);
            resolve(3);
        }, 100);
    });
}
var args = [];
var addResult = function(arg) {
    args.push(arg);
};

// Run sequentially
asyncOne()
  .then(addResult)
  .then(asyncTwo)
  .then(addResult)
  .then(asyncThree)
  .then(addResult)
  .then(function() {
    console.log(args);
}).catch(function(error) {
    console.log(error);
});

// Run in parallel
Promise.all([asyncOne(), asyncTwo(), asyncThree()]).then(function(args) {
    console.log(args);
});


When working with Asynchronous code in JavaScript, promises is a must use feature, it will greatly improve the flow and structure of your code, while bringing some much needed sanity to your asynchronous code.


Additional reading:

You're Missing the Point of Promises 
Javascript promises and why JQuery implementation is broken
JavaScript Promises There and back again


Leave a Comment