How to keep your promises: JavaScript edition

← back to the blog

The first time I needed to access a third-party API on the web I remember doing something like the following (in PHP):

$data = file_get_contents('http://a/resource/somewhere');

This has the advantage of being really easy to read: like all other parts of my code, I assign a variable to an expression and expect that it will be filled with a value of some sort. What I never thought about at the time was how much was going on behind the scenes: unlike an basic expression that performs a math operation or comparison, a whole HTTP request and response is going on before $data can be filled. During this time, my program pauses and no further code is executed.

Enter JavaScript. In a browser, this same operation would be unacceptable: retrieving a resource through AJAX would mean no code can be executed until the response arrives, meaning the the user interface would be for all intents a purposes frozen. On a Node.js server (which is single-threaded), no new client requests would be processed.

Instead, in JavaScript, the equivalent function would accept an additional argument which is the callback function, which would execute when the response from the third-party is received. It does not return a value at all. This asynchronous execution has the advantage that slow operation like HTTP requests don't tie up the processor, but they have the disadvantage that they can quickly get out of hand, as we will see.

In a second pattern, the function does return a value, but it is not the HTTP response itself. Rather, it is an object which can be thought of as a representation of an unknown value. This object is called a promise. A promise has methods called handlers that describe what to do when the promise is either fulfilled or rejected.

This post is not meant to be an exhaustive treatment of promises, but merely an introduction to why I have found them useful. It is intended for those who know some JavaScript but are not experts at writing asynchronous code.

I will deal exclusively with the ES6 (Promise/A+) variety. Since they are formally part of ES6, you may need a polyfill for older browsers. They are standard current version of Nodejs, however.

Examples: Wrapping Callbacks in a Promise

Let's look at perhaps the simplest example of a function that executes an asynchronous callback: setTimeout. This function accepts two parameters: a callback to execute when a time interval completes, and a number representing a length of time in milliseconds. So

setTimeout(function() {
    console.log('Some time later ...');
}, 1000);

gets the point across.

Let's wrap write a new function so that we can use a promise to execute a timed event. We'll call it wait.

function wait(time) {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            resolve('You kept your Promise!');
        }, time);
    });
}

wait(1000).then(function(msg) {
   console.log(msg)
});

The key is return new Promise. To this you pass a function with two parameters: one is function to resolve (fulfil) the promise and the other is function to reject the promise (the reject function is not used in the example above, because there's really no way for setTimeout to go wrong, but we'll see more example below that use it). Whatever values are passed to resolve and reject will later be passed to the handlers.

Note that we can't do this for setInterval, which executes a recurring event. A promise can be resolved or rejected: after that, it's gone.

This example is pretty trivial. First of all, there's no real unknown value that we need the promise to keep track of for us and secondly, there's virtually no possibility of this promise being rejected.

Let's look instead at Node's http.get, which is basically the equivalent of PHP file_get_contents mentioned above.

http.get('http://a/resource/somewhere/', function(response) {
   // deal with response
});

Let's create a smarter promise:

function requestResource(url) {
    return new Promise(function(resolve, reject) {
        http.get(address, function(response) {
            if (response.status === 200) {
                 return resolve(response.body);
            else {
                 return reject('Response not OK');
            }
        });
    });
}

We've already made our lives easier when we need to handle the promise. For one thing, we are only passing the response body, which might be all we're going care about later. Secondly, we are rejecting the promise if the status code is not 200 (IRL we'd probably want to a better job and resolve some other codes, but that's a story for later). We can handle a rejection with the .catch method, which works just like .then.

Let's use the Dark Sky weather API to get the current ski conditions

requestResource('https://api.forecast.io/forecast/APIKEY/50.1149639,-122.9486474').then(function(res) {   
    console.log('The current temperature is ' + res.body.currently.temperature);
}).catch(function(err) {
    console.log(err)
});

Note: you'll have to use a real API key to make this work. The code will print out the current temperature in Whistler, BC, Canada.

Why would you want to do this?

All well and good, but so far, our promises have required us to write a fair bit of code to achieve pretty simple results. Why would you want to use them?

Avoid excessive nesting. The most oft-cited advantage of promises is that they avoid the infinitely-nested callbacks that are so common in Node.js programming. In a web server-type application, the callback to respond to an HTTP request itself often contains a callback to query a database:

router.get('/my-page', function(req, res) {
    getSomeDataFromDb(req.someParameter, function(data) {
        doSomething(data);
        res.json(data);
    }
});

This is two indentations deep before we even get to the meat, and it only involves one database query and no extra processing. On the other hand, we can chain promises to keep each step at the same lexical level as the step before it:

myPromise.then(function(data) {
   return doSomething(data);
}).then(function(newData) {
    return doSomethingElse(newData) {
}).then(function(newNewData) {
    return doSomethingElseElse(newNewData);
});

You get the idea. At each step, the return value is the parameter passed to the next .then's callback.

Returning Promises. Even more useful than chaining promises in the same function is returning a promise and doing further processing in some other part of your code. The promise is an object that can be passed between functions. This helps keep functions simple and logical. We have already seen the pattern of returning a promise above. Since the handler (.then) also returns a promise, we can tack on more handlers in each function the promise is passed to.

Callbacks cannot be chained. A function that executes a callback that it does not itself define must accept that callback as an argument.

function stepOne(callback) {
    stepTwo(callback);
}

function stepTwo(callback) {    
    setTimeout(callback, 1000);
}

stepOne(function() {
    console.log('I got there ... eventually');
});

Easier to keep track of multiple asynchronous events. I have found that when multiple asynchronous events are involved, Promises have a few advantages. First of all, thanks to JavaScript's scoping rules, the resolve and reject methods can be more than one level deep when they are executed. Imagine you need to make two database queries, and the second is dependent on the data returned by the first:

function queryDb(params) {
    return new Promise(resolve, reject) {
        getDataFromDb(params, function(data) {
            getMoreDataFromDb(data, function(moreData) {
                resolve(moreData);
            });
         });
    });
}

queryDb.then( ... )

We've taken something pretty hairy and wrapped it a function which lets you not worry about the details too much when you use it later.

Another common pattern is returning promises in a then handler:

myPromise.then(function(dataOne) {
    // process dataOne and create another promise
    return myOtherPromise;
}).then(function(dataTwo) {
    // do something with dataTwo
});

Somewhat surprisingly (but very usefully), dataTwo is NOT myOtherPromise itself, but rather the value that myOtherPromise resolves to. This lets us be a little bit dumb about creating promises within handlers, secure in the knowledge that the next handler won't be invoked until the value we care about it known.

Finally, if you have several operations and don't care what order they complete in, Promise.all accepts an array of Promises and creates a new Promise that resolves when all of them complete. This is useful for tying several things together and only proceeding once everything is done.

Gotchas

Gotcha the 1st: resolve() is not return. It's important to note that invocation of resolve is not the same as return. In other words, the initial function keeps executing until the end of the function or a return statement is reached. The value passed to resolve cannot be modified after calling it, so it's possible to use promises for quite a while without encountering this little quirk, but it's an important thing to keep in mind as it can lead to some unexpected behaviour.

For this reason, I always use return resolve() unless there's a compelling reason not to.

Gotcha the 2nd: Errors that get swallowed. We have not yet talked about errors very much. In many environments, exceptions thrown within a promise will silently disappear, without even producing an alert in the console. Obviously this can lead to real frustration.

The standard way to handle errors and rejected promises is with the .catch method.

new Promise(function(resolve, reject) {
    throw new Error('This is an error');
}).catch(function(err) {
  console.log('An error occurred');
});

The output above will be 'An error occurred', but in most cases, there will no output at all without the .catch.

Fortunately, catch catches all previous errors in the chain, so appending it to the end of a chain will ensure that any errors don't get swallowed.

Gotcha the 3rd: Errors that you miss There are in fact two ways to catch errors: the .catch we have just seen, and a form of .then that accepts two arguments:

myPromise.then(function(result) {
    handleResult(result);
}, function(err) {
    handleErr(err);
});

The important thing to know about this form is that either the first function or the second will be invoked, but never both. So if an error occurs inside the first function passed to .then, we need an error handler further down the chain. For this reason, I recommend using .catch unless there is a really good reason not to.

Conclusion

Promises are a great addition to the JavaScript toolset and make working with asynchronous code a little more fun. This has been a summary of the most important things I've encountered: for a really thorough treatment on getting started see this post by Jake Archibald or the MDN reference page on promises.

Changelog

2016-03-19 Thanks to Steven Parker I've added a couple of clarifications on reject and catch.