Promises in JavaScript

Recently, I got to be quasi-creative with a challenge that came up at work, but not as much as I’d hoped. I wrote about it here. For brevity, it required the use of a JavaScript loader to dynamically require external files not managed through our Webpack configuration.

There are a ton of JavaScript loaders available, few of them seem reliable. However, during my research for the right one, I noticed a trend of APIs using Promise-Based Architecture. Ultimately using Promises proved to be overkill for my particular problem, which involved making a single request. But, since the announcement that Promises have arrived natively in JavaScript, it got me curious about what use cases would benefit from using this design pattern to write cleaner, asynchronous code.

Consider the following synchronous function to read a file and parse it as JSON:

function readJSONSync(filename) {
return JSON.parse(fs.readFileSync(filename, 'utf8'));
}

This is nice and easy to read, but its not the Node.js way as it’s blocking, thus preventing the execution of any other operation while the file is being read.

In Node, the idea is for your code to be performant and responsive and I/O operations that are asynchronous (read: non-blocking) allow us to architect our applications in such a way:

function readJSON(filename, callback){
fs.readFile(filename, 'utf8', function (err, res){
if (err) return callback(err);
callback(null, JSON.parse(res));
});
}

This is the same function written asynchronously using a callback. This is fine, but there are side effects that are inherent when using callbacks

The callback does not make it clear what is input and what is the expected returned callback. Also, how are we to handle any errors thrown by JSON.parse? By the time we throw in error handling, this singular operation makes our code look like a bowl of spaghetti:

function readJSON(filename, callback){
fs.readFile(filename, 'utf8', function (err, res){
if (err) return callback(err);
try {
res = JSON.parse(res);
} catch (ex) {
return callback(ex);
}
callback(null, res);
});
}

Now, we don’t use Node at LendingTree, but this is obviously useful on the front end when moving towards more modular architecture.

Promises represent an alternative system for making asynchronous requests.

Callbacks are great when making one request, with one result, to do one thing, but in the real world its a lot more convoluted.

For example, you will likely make multiple requests simultaneously and the UI logic may differ depending on which request returns first.

Or perhaps you want to separate server API calls from the code that handles the data once it’s returned.

Promises make it easier to manage multiple async requests and makes your code much cleaner when using real world applications.

A brief history lesson…

Promises are not a new concept. The terms future, promise can be traced back to 1976 as a construct in earlier functional programming languages to abstract values (a future) and how they are computed (a promise).

Promises in JavaScript have been around for a while in the form of libraries such as Q.js, when, WinJS, or RSVPjs. JQuery has something similar called Deferreds that is not Promise/A+ compliant and is thus less useful.

What is a promise?

The official definition of a Promise straight from MDN is:

An object used for asynchronous computations that represents a value which may be available now, or in the future, or never.

It basically lets you write logic around data that you have not yet, or may never, receive.

To create a promise object, simply call the Promise constructor, passing in a function that begins the async operation:

let thisPromise = new Promise(function(resolve, reject) {
// perform an async operation
  if (/* everything turned out fine */) {
resolve("Stuff worked!");
}
else {
reject(Error("It broke"));
}
});

The Promise constructor takes a callback as a singular argument that, in turn, takes two parameters: resolve and reject.

The Promises/A+ spec is the smallest piece of documentation I’ve ever seen and I think the concept of creating this loving document is to standardize how developers implement Promises when authoring libraries. And there are just a few concepts to really grasps for them to make sense to start using right now.

Promises Have State

Promises in JavaScript are sort of like promises in real life. Your kid makes a promise to clean the room by the end of the day. He fulfills the promise by cleaning the room, or he doesn’t and fails.

Thinking along those lines, know that promises can be one of only three states.

Pending when the final value is not yet available .

Your kid has committed to cleaning the room by the end of the week. It is yet to be seen, however, whether he will complete the room. This state transitions to either:

Fulfilled when and if the final value is available.

You’re kid has successfully cleaned the room — or:

Rejected, when an error occurs and prevents the final value from being determined.

You’re kid simply fails to clean the room at the end of the day.

Promises are Composable

Operationally, promises are composable wrappers for callbacks. They are useful for sequencing synchronous and asynchronous actions.

The primary method when calling a Promise is its then() method, which registers callbacks to access either the eventual value or the reason why the promise failed to resolve.

promise.then(onFulfilled, onRejected)

This method returns a Promise and takes up to two arguments: callback functions for both the success and failure cases of the Promise, which allows for method chaining.

Here is a more complicated example using callbacks. When written in this fashion, you begin to see problems with nesting and the amount of overhead needed for error handling.

require("request")
.get("http://www.google.com", function(err, response) {
if (err) {
console.error(err);
} else {
require("fs")
.writeFile("google.html", response.body, function(err) {
if (err) {
console.error(err);
} else {
console.log("wrote file");
}
})
}
})

And below is the same operation using Promises. Instead of nesting we get a straightforward linear progression of events.

require("request").get("http://www.google.com")
.then(function(response) {
return require("fs-promise").writeFile("google.html", response.body);
})
.then(function() {
console.log("wrote file");
})
.catch(function(err) {
console.log(err);
})

The syntax is so clean, and almost looks synchronous; and like synchronous operations, you can rely on exception propagation to handle errors in a central location like try/catch.

This is why we use Promises; to gain control over function execution order, to make your code much more composable and easy to reason about.

That’s about all you need to know to get started using promises.

There are some static methods that I’ve come to think of as utilities to make calling promises easier.

Promise.all([iterable]) promise.all iterates over an array of promises and once all of them are fulfilled it fulfills with an array of their values; conversly if one fails, it rejects with the value of the promise that rejected, whether or not the other promises have resolved.

Promise.race([iterable]) is useful of you don’t care which promise is resolved or rejected, or when, just that it happens. It immediately returns a promise with the status of the first rejected or fulfilled promise.

Further Reading:

JavaScript Promise: an Introduction — Lovely Introduction from Google

Promise — JavaScript|MDN

Three Ways of Understanding Promises — Dr. Axel Rauschmayer, 2ality