How JavaScript promises work

Learn why JavaScript promises are like a gift box and get up to speed with JavaScript promise syntax

How JavaScript promises work
2Mmedia / Getty Images

Last week we looked at callbacks and promises. I made a case for using promises to easily coordinate asynchronous code. This week, we’ll dig further into understanding promises and look at the syntax.

Imagine you have a box. You don’t know if there's anything in the box yet, but you can carry it around and say things about it, like:

“This box is meant to have a gift inside.”
“If the gift is in the box, I should tell everyone the gift is here.”
“If the gift is in the box, I should also write a thank you note.”
“If there’s a letter in the box saying the gift was out of stock, I should go order a different gift.”

You don’t need to know if there’s a gift inside, or a letter, or if the box is empty to say any of those things. Now imagine a gift arrives and is placed in the box. You can then tell everyone that the gift is in the box and write a thank you note. You also still have the box, so you can continue to say things like:

“If the gift is in the box, I should do a dance.”

As soon as you say that, you can look in the box, see the gift is there, and then do a dance.

The box is a container for something that may already exist or may exist in the future. The contents of the box could be what you were expecting, or something that indicates you’re not getting what you wanted. The box is a promise of a gift. Ideally the promise is honored and you get your gift. Unfortunately for all gift recipients, promises are sometimes broken. You may learn that your gift will not be coming after all.

JavaScript promise syntax

A promise in JavaScript is just like that box. It has an intended value that may or may not arrive, or instead of the intended value it might contain some sort of error. Let’s take a look at the syntax of a JavaScript promise, using the gift box scenario above to drive our example. Here is the full scenario described in code:

let giftPromise = new Promise((resolve, reject) => {
  $.ajax('/gift', {
    success: (response) => resolve(response),
    error: (jqXHR) => reject(jqXHR.responseText),
  });
});
giftPromise.then(() => console.log('The gift is here!'));
giftPromise.then(writeThankYouNote);
giftPromise.catch(orderAnotherGift);

/* At some point in time later, after the gift arrives and we’ve logged our message and written a thank you note */
giftPromise.then(doADance);

Let’s dig into the syntax of promises so we can understand how to read the example above. First of all, Promise is a constructor that's called with the new keyword to create an instance of a promise. The constructor takes a single argument that must be a function. The most minimal (and mostly useless) promise declaration is:

let giftPromise = new Promise(() => {});
/* We can see that our giftPromise is an instance of a promise */
console.log(giftPromise instanceof Promise); // Logs true

Try running this example in your browser console.

The function argument to a promise is called an “executor.” An executor determines what code should be run and under what circumstances the promise succeeds or fails. A successful promise is considered “fulfilled” and an unsuccessful one is considered “rejected.” In either case, the promise is “settled.” If we're still waiting for a result either way, the promise is “pending.”

The executor is handed two arguments, resolve and reject, which are both functions. You can use resolve and reject to tell the promise to become fulfilled or rejected, respectively. You can pass a single value of any type to these two functions, which will be passed to handler functions (discussed below).

The resulting promise has a handful of methods on it, but we're going to focus on just two: then and catch. then is a method that takes two functions as arguments. The first function will be called once when the promise is fulfilled. If the promise is already fulfilled, the function will be called almost immediately (at the end of the current event loop). The second function works the same way but will be called when the promise is rejected. catch is a shorthand way of defining a function to be called when the promise is rejected. If resolve or reject were passed a value, that value will be present as the first argument of the function.

A JavaScript promise example

Let’s look at a simplified example of a promise that you can play around with in your browser console. The promise below will wait 10 seconds, then resolve if the current time stamp is even or reject if the current time stamp is odd. 

let timerPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    let timestamp = Date.now();
    if(timestamp % 2 === 0) {
      resolve(timestamp);
    } else {
      reject('Error. Timestamp is odd.');
    }
  }, 10000);
});
timerPromise.then(
  (ts) => console.log(`Timer promise resolved successfully with the timestamp: ${ts}`),
  (errorMsg) => console.log(`Timer promise rejected with the message: ${errorMsg}`));

What do you think would happen if you added a new handler after a promise resolves or rejects? To test your guess, try adding then and catch handlers both before and after the promise has been fulfilled or rejected. If you add the handler afterward, you’ll have to use something like a setTimeout to ensure that the promise resolves before the handler kicks in, like so:

let timerPromise = new Promise(/* Existing promise code from before */);

// Wait 15 seconds before adding a new handler
setTimeout(() => {
  timerPromise.then(
    (ts) => console.log(`Late handler: resolved successfully with the timestamp: ${ts}`),
    (errorMsg) => console.log(`Late handler: rejected with the message: ${errorMsg}`));
}, 15000);

Next week, we’ll talk about using async and await to handle promises in a cleaner, more readable way.

For more detailed technical information, check out the MDN Web Docs pages on JavaScript promises. As always, reach out to me on Twitter with any comments or questions.

Copyright © 2018 IDG Communications, Inc.