Mastering Asynchronous Javascript: Part 2 – Promises

Last time I introduced you to asynchronous programming, and talked about JavaScripts most basic mechanism for dealing with this, callbacks. As we saw last time, code readability and easy error handling were lacking when using callbacks. In this post I will teach you about probably the most widely used solution to this problem: promises.

Promises In JavaScript Are A Bit Like Real World Promises

Think of the day to day usage of the word ‘promise’. Let’s take an example. A father promises to take his daughter to the fair on Friday. Since this promise relates to a future event, it can said to be in one of three distinct states at any given time:

  • Pending: It’s not yet Friday, so we don’t know if the father will keep his promise yet or not.
  • Fulfilled: Friday comes and the father takes his daughter to the fair. Candy floss and fun abound.
  • Broken: Friday comes, but the father fails to take his daughter to the fair. There are tears and a temper tantrum.

Now, let’s apply the concept of a promise to something related to software development. A good example of something that is a bit like a promise is an HTTP request:

var resultPromise = fetch('http://api.mywebsite.com/user/1');

The fetch function makes HTTP requests asynchronously. In other words, JavaScript does not wait for a response for the request. It carries on doing other things, and then informs you when the response arrives. Read more about the event loop in the previous post if this is confusing to you.

This means that the result of fetch is like a promise. It is some future event that can be:

  • Pending: The request has not returned yet
  • Fulfilled: The result is returned successfully
  • Broken: There is an error (e.g. the URL is not found)

In JavaScript a promise is an object which represents the state of a result of an operation. The possible states roughly follow the previous examples:

  • Pending: Don’t know if the result is a success or not yet
  • Resolved: Like fulfilled, it means the operation was a success
  • Rejected: A broken promise; the result was an error

A Basic Promise

A promise is created as follows:

var myPromise = new Promise(function (resolve, reject) {
    setTimeout(function () {
        resolve();
    }, 3000);
});

We construct a new promise object. The constructor for the promise object takes a single argument, which is a function. This function has two arguments: resolve and reject. Each of these arguments are themselves functions. We call resolve() to resolve the promise, or reject() to reject it. Before either resolve or reject are called the state of the promise will be ‘pending’. After reject is called, the state will be ‘rejected’. After resolve is called, the state will be ‘resolved’. Each of these functions can only be called once, and you can’t call both of them, it’s either or. In other words once the promise has been resolved it cannot be either rejected, or resolved again, and visa versa. In the example we set a timeout for 3 seconds, and when that time is passed, the promise will be resolved. Until then it will be pending.

This seems pretty useless. The promise object knows its state, but how can that help us in responding to asynchronous actions? This is where two methods on the promise object come in: then and catch.

var myPromise = new Promise(function (resolve, reject) {
    setTimeout(function () {
        resolve('All ok!');
    }, 3000);
});

myPromise
    .then(function (result) { console.log(result); } )
    .catch(function (err) { console.log(err); })

Paste the above code into the JavaScript console in chrome. After 3 seconds ‘All ok!’ should be output to the console, as the then callback is called when the promise resolves. Next paste the following slight variation into the console:

var myPromise = new Promise(function (resolve, reject) {
    setTimeout(function () {
        reject('There was an error!');
    }, 3000);
});
 
myPromise
    .then(function (result) { console.log(result); } )
    .catch(function (err) { alert(err); });

This time ‘There was an error!’ will be shown in an alert dialog after 3 seconds. This is because this time, we rejected the promise, and so the function passed to catch was called. From this we can see that then assigns a resolved handler function, and catch a rejected handler function. In both cases the argument that was passed to either resolve or reject are callbacks for each state. These callbacks each take an argument. This allows us to extract a result from the asynchronous operation in the case of success, or a meaningful error in the case of failure.

Promise Chaining

So far this may just look like callbacks all over again, except with promises there is one callback for success and another for failure, instead of a single callback. The real win for promises and the way they solve the problems with callbacks is that promises can be chained.

To show you just what this means, I’ll rewrite the database interaction example used in the callbacks post to use promises instead.

var promisify = require('promisify-node');
var MongoClient = promisify('mongodb').MongoClient;

var url = 'mongodb://localhost:27017/myproject';
function output (user, orders) {
    // output data in pretty format
}

function logError (error) {
    // Log the error (could be console, a service, a file etc)
}

let db = null;
const userId = process.argv[2];
let user = null;
MongoClient.connect(url)
    .then(function (connection) {
        db = connection;
        return db.collection('access_log').insert({ userId, date: new Date() });
    }).then(function () {
        return db.collection('users').find({ userId }).toArray();
    }).then(function (users) {
        user = users[0];
        return db.collection('orders').find({ userId }).toArray();
    }).then(function (orders) {
        output(user, orders);
    }).catch(logError);

This code does the same as the code in the last article, except with promises instead of callbacks.

First I use the promisify-node module to convert the mongodb module to use promises instead of callbacks. I would usually use a database module that uses promises out of the box (like Mongoose, for example), but wanted to keep the code as similar to that of the last post as possible to avoid confusion.

Now all the mongo functions such as connect, insert and find will return a promise instead of taking a callback as their last argument. Whenever these functions lead to an error, the returned promise will be rejected. If the operation completes successfully, it will resolve the promise instead.

Notice how all the promises are chained together like this .then(…).then(…).then(…). This is made possible because if a then handler returns a promise, then the next then() will take on the state of that returned promise. We therefore have a chain of promises. This makes code much more reasonable, because we don’t have everything creeping over to the right of the screen with multiple indentations, and remove confusion as to when blocks end. In short it gets rid off all that nasty nesting we get with callbacks.

The other big win is that we don’t have to constantly check for errors. If any of the promises are rejected, this rejection will bubble up through all the calls to then until they hit the catch at the end, at which time the error is passed to logError.

Next Time

I’m sure you’ll agree that promises are nicer than callbacks. Still, thanks to new functionality added recently to JavaScript there are ways of handling asynchronicity that I feel are even better. Join me next time when I’ll introduce you to these new additions to the JavaScript language: generators.

The full series:

  1. Callbacks
  2. Promises
  3. Generators
  4. Async / await
  5. RxJS

Leave a Reply

Your email address will not be published. Required fields are marked *