Understanding JavaScript Promises

Article covering a brief overview of JavaScript Promises and how to use them

Published July 21, 2022

What is a Promise?

In everyday language, a promise is a declaration or assurance that one will do a particular thing or that a particular thing will happen, and it can either be kept or broken. In JavaScript, a Promise is similar: it's simply an object with a value that will either be fulfilled or rejected. If a Promise is fulfilled, the application will do this; if the Promise is rejected, the application will do that.

JavaScript Promises are most commonly used when handling asynchronous operations. It's important to have a strong understanding of synchronous and asynchronous programming before working with Promises. If you don't understand asynchronous JavaScript, I have an article which explains it in full detail here:

How are Promises used?

Let's say that you need to make an API request in order to fetch some data. Retrieving data is something unpredictable and out of your control — it can be fast, slow, successful, or even fail completely. Because of this, API requests must be handled asynchronously, typically through a Promise. Promises ensure that the correct course of action is taken, regardless of whether the request is successful or not.

How do you create a Promise?

Generally, an instance of a Promise is created by using the new keyword followed by the Promise() constructor function. This function accepts a function as its argument, which looks like this:


const promise = new Promise(() => {
// ...
});
        

...this function takes in two arguments, resolve and reject, which are also functions...


const promise = new Promise((resolve, reject) => {
// ...
});
        

...one thing to note is that a Promise has 3 different states...

...with this in mind, resolve() is a function that when called, changes the state of the Promise from pending to fulfilled. On the other hand, reject() is a function that when called, changes the state of a Promise from pending to rejected.

As stated earlier, Promises are generally used when working with data, but for simplicity's sake, I'll use the following example:


const promise = new Promise((resolve, reject) => {
  let age = 17;

  if (age >= 21) {
    resolve();
  } else {
    reject();
  }
});
        

...here's a Promise that uses a conditional statement. If age is 21 or greater, the Promise will be fulfilled; if age is below 21, the Promise will be rejected. If the Promise is fulfilled, I want old enough to drink logged to the console; if the Promise is rejected, I want too young to drink logged to the console. In order to do this, I'll create two separate callback functions — one for handling the fulfilled state and another for handling the rejected state...


// ...
const oldEnoughMessage = () => {
  console.log("old enough to drink");
};

const tooYoungMessage = () => {
  console.log("too young to drink");
};
// ...
        

...now these functions can be combined with the Promise, but how?

.then() and .catch() methods

oldEnoughMessage and tooYoungMessage are both callback functions, which means they are passed in as arguments to other functions. Those other functions are the .then() and .catch() methods, which the Promise constructor function gives us access to.

Remember when I said that a Promise's course of action is determined by its success or failure? Well, those actions lie in the .then() and .catch() methods. If the Promise is fulfilled (age is equal to or greater than 21), the .then() method/function will be executed. If the Promise is rejected (age is less than 21), the .catch() method/function will be executed.

With this in mind, we can pass in each callback function accordingly, like so:


const promise = new Promise((resolve, reject) => {
  let age = 17;

  if (age >= 21) {
    resolve();
  } else {
    reject();
  }
});

const oldEnoughMessage = () => {
  console.log("old enough to drink");
};

const tooYoungMessage = () => {
  console.log("too young to drink");
};

promise.then(oldEnoughMessage);
promise.catch(tooYoungMessage);
// too young to drink
        

...alteratively, the exact same result can be accomplished by passing in each callback function as an arrow function...


const promise = new Promise((resolve, reject) => {
  let age = 17;

  if (age >= 21) {
    resolve();
  } else {
    reject();
  }
});

promise.then(() => {
  console.log(result);
  console.log("old enough to drink");
});

promise.catch(() => {
  console.log(error);
  console.log("too young to drink");
});
// too young to drink
        

...the latter is the most common way of doing it. If you take a look at the code, you'll notice that the resolve() and reject() functions are still empty. In order to make use of the value, I'll pass in a string to each one...


// ...
if (age >= 21) {
    resolve('Success!');
  } else {
    reject('Error!');
  }
// ...
        

...to access these strings, just pass them as arguments to the callback functions...


const promise = new Promise((resolve, reject) => {
  let age = 17;

  if (age >= 21) {
    resolve("Success!");
  } else {
    reject("Error!");
  }
});

promise.then((result) => {
  console.log(result);
  console.log("old enough to drink");
});

promise.catch((error) => {
  console.log(error);
  console.log("too young to drink");
});
// Error!
// too young to drink
        

...this works perfectly, but the code can still be cleaned up even further.

Promise chaining

.then() and .catch() both return Promises, so they can be chained together, which is referred to as "Promise chaining". Promise chaining allows you to chain together multiple .then() calls, which will run when the previous Promise is fulfilled. The .catch() method can still be called to handle any errors along the way. Promise chaining usually looks something like this:


const promise = new Promise((resolve, reject) => {
  let age = 22;

  if (age >= 21) {
    resolve("Success!");
  } else {
    reject("Error!");
  }
});

promise
  .then((result) => {
    console.log(result);
    console.log("old enough to drink");
    const msg = "🍻";
    return msg;
  })
  .then((drinks) => {
    console.log(drinks);
  })
  .catch((error) => {
    console.log(error);
    console.log("too young to drink");
  });
// Success!
// old enough to drink
// 🍻
        

...in this example, age is 22, which means the Promise will be fulfilled, therefore running the first .then() callback function, which logs Success! and old enough to drink to the console. This callback function also returns a variable called msg, which is then passed as the drinks argument to the chained call, where it finally gets logged to the console. If age was less than 21, only the .catch() callback function would be invoked.

That's JavaScript Promises in a nutshell. Although there's nothing wrong with using .then() and .catch() for handling asynchronous code, ES7 introduces the async and await keywords, which have an even cleaner syntax, all while offering the same functionality. In my next article, I'll be covering how to use async/await to enable asynchronous, Promise-based behavior, as well as comparing it to .then()/.catch(), so stay tuned!