The easy guide to understanding JS Promises

Mehdi Maujood
Salesforce Zolo
Published in
9 min readApr 6, 2019

I’ll be honest. I struggled to understand promises when I first started learning about them. I had to walk through multiple articles and examples and experiment a good bit to get a handle on what was going on. I felt that most explanations I read left too much to the reader and didn’t impart the justice demanded by any complex idea: breaking it down into small, understandable parts.

The goal of promises is to write cleaner code by eliminating the excessive use of callback functions. If you’ve spent some serious time with JavaScript you may be familiar with something we call the “callback hell” which is something that starts with you passing lots of functions around and ends with you lamenting every single decision that led to the creation of JavaScript. Just ask this guy Max Ogden — he apparently got so upset with the callback hell that he made a website about it: callbackhell.com.

We do have a big-brained elite among us who just need to go over the MDN page on using promises and they’re churning out promise-based code right after. I evidently was not one of them and if you’re like me, keep reading. I promise you’ll walk away with a good understanding of promises.

Getting started with promises

To put it as simple as possible, a promise is an object that represents an operation. Usually, promises are used to represent asynchronous operations like AJAX calls or an animation.

Since the above two sentences do absolutely nothing to further our understanding of promises, let’s try putting together an example of using a promise object to represent a very simple operation:

function doLog() {
console.log('Hello World!');
}

doLog is a function, and functions are operations. This is how we build a promise object to represent doLog:

var myPromise = new Promise(doLog);

All we did is we passed doLog to the promise constructor and we now have a promise object representing the task to be carried out by doLog. And what happens when we execute the above line? We see the following in the console:

Do you see what happened when you created a promise representing the function doLog? Creating a promise object just called the function.

Think about that for a moment. new Promise(doLog) had the exact same effect as doLog(). The promise constructor just ran the function passed to it.

The Promise object

In order to further our understanding of what a promise object does, let’s see what information the myPromise object holds. If I type myPromise in the console, I see this:

myPromise has two properties — [[PromiseStatus]] and [[PromiseValue]]. The status is pending while the value is undefined.

Why is the status pending? The operation this promise represents, doLog, is complete. Shouldn’t the status be “completed” or something?

The reason the status is pending is that the promise object doesn’t know that the task is complete. As far as the promise is concerned, doLog may have sent out an AJAX call, set off a timer or started an animation that will take a few seconds to finish. There is no way for the promise object to know this — all it knows is that it was passed a function to execute.

It is doLog’s responsibility to tell the promise object when it’s done, and the promise object provides us with a way to do that. Take a look at the following modification to our code:

function doLog(resolve, reject) {
console.log('Hello World!');
resolve();
}
var myPromise = new Promise(doLog);

When the promise constructor calls doLog, it passes to it two functions that we’ll name resolve and reject. doLog can call resolve to let the promise object know that the task is complete, or call reject to let the object know that our task failed. Until resolve or reject are called, the promise would stay pending.

When the above piece of code finishes executing, the status of myPromise will be completed.

Experimenting with a long-running task

This is where things start getting interesting. Promises are designed to represent asynchronous processes, so let’s try our experiment with something like a timeout:

function doWork(resolve, reject) {
setTimeout(function () {
console.log('Work done');
resolve();
}
, 5000);
}
var myPromise = new Promise(doWork);

Notice the callback function I’ve highlighted in bold. It will be called 5 seconds after doWork is called (a timeout of 5 seconds) and myPromise will stay pending for 5 seconds. Once 5 seconds have passed, myPromise will be resolved. I would encourage you to hit F-12, put the code in in your console and see how myPromise changes state after 5 seconds.

.then()

We can call then() to attach functions to a promise object that will be called when a promise is either resolved or rejected. Take a look at the following piece of code:

function doWork(resolve, reject) {
setTimeout(function () {
console.log('Work done');
resolve();
}, 5000);
}
var myPromise = new Promise(doWork);
myPromise.then(function () {
alert('Success!');
}, function () {
alert('Error!');
});

Notice that we’ve passed two functions to then. The first function gets called as soon as the promise is resolved (or gets immediately called if the promise is already resolved) and the second function gets called when the promise is rejected. When we execute this code, we will see “Work done” in the console after 5 seconds, immediately followed by an alert message saying “Success!”. This will happen because the first function we passed to then will be called right when the promise is resolved. The second function would have been called if the promise was rejected.

The promise value

In the beginning of this article, we saw that the promise object has two properties — value and status. We now know exactly what the promise status is, but we haven’t taken a look at what the value property does.

To cut it short, we can pass a value to the resolve and reject functions, and whatever we pass to them gets set as the promise value. Furthermore, the value we pass to resolve or reject also gets passed into the functions we attach to the promise object using then. Let’s look at an example:

function doWork(resolve, reject) {
setTimeout(function () {
resolve('Work done!');
}, 5000);
}
var myPromise = new Promise(doWork);
myPromise.then(function (msg) {
alert('Success: ' + msg);
});

Let’s put all of this information together and create a promise to represent an AJAX call.

Putting it together: an AJAX call

This is how you would code an AJAX call using a Promise to represent it. For simplicity, I’m using jQuery’s ajax function.

function myAjaxCall() {
return new Promise(function (resolve, reject) {
$.ajax({
url: '/ajax/imaginary_url.html',
success: function (data) {
resolve(data);
},
error: function (xhr, status, error) {
reject(error);
}
});
});
}

Notice that we’ve passed the function directly to the promise constructor this time instead of declaring it first. Then somewhere later in the code:

myAjaxCall().then(function (data) {
//this will be called when the ajax call is successful
}, function (error) {
//this will be called if there's an error
});

I now wish to communicate to my readers an ah-ha thought that may have occurred after seeing the above:

Converting the AJAX call from callbacks to promises was utterly, completely useless.

It’s true. We could do the same just as easily with callback methods. Instead of attaching methods to the promise using then, we could just pass two methods into myAjaxCall and it would work just as well. It would perhaps be easier to do this AJAX call without involving a promise object.

So what really is the point of promises?

Promise chaining and avoiding the callback hell

The true power of promises comes from the ability to chain promises. This is possible due to the fact that the function then itself returns a Promise.

If you’re returning a promise from the handler you attached through then, the promise then returns will resolve when your returned promise is resolved. If you return a simple value from the handler you attached using then, then the promise returned by then will be in a resolved state with the returned value as the promise value. I know this sounds complicated, so let’s dive into an example:

//this function returns a promise that resolves in msec milliseconds
function wait(msec) {
return new Promise(function (resolve) {
setTimeout(resolve, msec);
});
}

The function call wait(3000) will return a promise that resolves in 3 seconds. The call wait(6000) will return a promise that resolves in 6 seconds. Now take a look at the following:

wait(3000).then(function () {
console.log('Waited 3 seconds!');
return wait(6000);
//notice that we're returning a promise
}).then(function () {
//The previous handler returned a promise
//This means that we will now wait for that promise to resolve
//When it's resolved, this handler executes
console.log('Waited 6 more seconds!');
return 5;
}).then(function (val) {
//the last handler returned a number
//this handler will be called immediately, as the promise
//returned by the last .then() call was resolve immediately
console.log('This will be called immediately');
console.log('The value passed is: ' + val);

});

I know the above may be a little hard to understand. I would encourage you to play around with it if it’s unclear as it’s very important that you understand exactly what’s happening in the code above before proceeding.

An example of better code through promises

Imagine that you have to send an AJAX call, launch two animations together after the ajax call returns, launch a third animation when the two animations finish and put data on the screen after the animations end. That’s four asynchronous operations. This is what it could look like with callbacks:

var oneAnimationComplete = false;doAjax(function (data) {
doAnimation1(function () {
onAnimationComplete(data);
});
doAnimation2(function () {
onAnimationComplete(data);
});
});
function onAnimationComplete(data) {
if (oneAnimationComplete) {
doFinalAnimation(function () {
renderData(data);
});
}
else oneAnimationComplete = true;
}

You can see that the code is not very intuitive. It’s not easy to see the sequence of the different operations because the different functions we’re passing around are scattered all over the place. Notice the clumsy way of ensuring that the last animation is called after both animation1 and animation2 are complete. A little more code like this and you’re deep into callback hell territory wishing you’d picked basket-weaving instead of programming.

With promises, and with special thanks to promise chaining, you can write it like this:

let data = {};
doAjax().then(function (d) {
data = d;
return Promise.all(doAnimation1(), doAnimation2());
//Promise.all returns a new promise that is resolved
//when all promises passed to it are resolved
}).then(function () {
return finalAnimation();
}).then(function () {
renderData(data);
});

You can see the big difference. These operations are expected to execute one after the other, and promises allow code to cleanly capture that flow. We don’t need to have messy nested callbacks anymore, we can now have lines of code one after the other just like we would in synchronous code. Do the ajax call, then do both animation1 and animation2, then do the final animation and then render the data. The code is readable and captures the actual flow of operations.

Notice the use of Promise.all — it returns a new promise that is resolved when all promises passed to it are resolved. Promise.all is one of the many bells and whistles that come with promises.

Here is an excellent article that reinforces the idea of promises allowing us to mimic synchronous code. Finally, don’t forget the ultimate resource: The Mozilla Developer Network section on promises.

With Aura Component APIs returning promises for asynchronous operations and the advent of JavaScript-focused Lightning Web Components, it is especially imperative for us Salesforce developers to arm ourselves with a proper understanding of promises. Promises have a lot to offer but can be difficult to get started with and I hope my article was a help here, but do remember to keep exploring.

--

--

Mehdi Maujood
Salesforce Zolo

Software engineer, Salesforce enthusiast, Pluralsight author