JavaScript Promises: A Simple and Clear Guide for Beginners

Introduction

Imagine you are booking a flight online. You enter your destination, departure date, and preferred time. You click the search button and wait for the results. What happens next? You don't just sit there and stare at the screen, hoping for the best. You probably do something else, like browse other websites, listen to music, or check your messages. Meanwhile, the flight website is searching for the best deals, contacting the airlines, and filtering the options. When the results are ready, you get a notification, and you can choose your flight.

This is an example of asynchronous programming, which means doing multiple things at the same time, without blocking the main thread of execution. JavaScript is a single-threaded language, which means it can only do one thing at a time. However, it can use callbacks, events, and promises to handle asynchronous operations, such as fetching data from a server, reading a file, or performing a computation.

What is a promise?

A promise is an object that represents the eventual outcome of an asynchronous task. An asynchronous task is a task that can take some time to complete, and does not block the main thread of the program.

A promise can have one of the following states:

  • Pending: the promise is not settled yet, and the outcome is unknown.
  • Fulfilled: the promise is settled successfully, and the outcome is a value.
  • Rejected: the promise is settled with an error, and the outcome is a reason.

How to create a promise

A promise can be created using the new Promise() constructor, which takes a function as an argument. This function is called the executor, and it has two parameters:

  1. resolve: The resolve parameter is a function that can be called to fulfill the promise with a value.
  2. reject: The reject parameter is a function that can be called to reject the promise with a reason.

For example, the following code creates a promise that simulates a flight search:

Javascript
                        
// Create a new promise
let flightPromise = new Promise((resolve, reject) => {
  // Simulate a random delay
  let delay = Math.floor(Math.random() * 10000);

  // Simulate a random success or failure
  let success = Math.random() > 0.5;

  // Wait for the delay to finish
  setTimeout(() => {
    // If success, resolve the promise with the flight details
    if (success) {
      resolve({
        airline: "AirBnB",
        price: "$199",
        duration: "3h 15m",
      });
    }
    // If failure, reject the promise with an error
    else {
      reject(new Error("No flights available"));
    }
  }, delay);
});

In our example, the flightPromise object is a promise that will either be fulfilled with an object containing the flight details, or rejected with an error object, after a random delay. However, creating a promise is not enough. We also need to use it somehow.

Using Promises

To use a promise, we need to attach handlers to it, which are functions that will be executed when the promise is settled. There are two main methods to attach handlers: .then() and .catch().

The .then() Method

The .then() method takes two arguments: a fulfillment handler, which is a function that will be called if the promise is fulfilled, and an rejection handler, which is a function that will be called if the promise is rejected. The .then() method returns a new promise, which can be chained with another .then() method, or a .catch() method.

For example, the following code uses the flightPromise object and attaches handlers to it:

Javascript
                        
// Use the flight promise
flightPromise
  // Attach a fulfillment handler
  .then((flight) => {
    // Log the flight value
    console.log(flight);
    // Return a new promise that resolves with a hotel
    return new Promise((resolve, reject) => {
      resolve({
        name: "Hotel California",
        rating: "⭐⭐⭐⭐",
        price: "$99",
      });
    });
  })
  // Attach another fulfillment handler
  .then((hotel) => {
    // Log the hotel value
    console.log(hotel);
    // Return a confirmation message
    return "Your booking is confirmed!";
  })
  // Attach a rejection handler
  .catch((error) => {
    // Log the error value
    console.error(error);
    // Return a cancellation message
    return "Your booking is cancelled.";
  })
  // Attach a final handler
  .then((message) => {
    // Log the message value
    console.log(message);
  });

The output of this code will depend on the random success or failure of the flightPromise object. If it is fulfilled, the output will be something like:

Javascript
                        
{ airline: 'AirBnB', price: '$199', duration: '3h 15m' }
{ name: 'Hotel California', rating: '⭐⭐⭐⭐', price: '$99' }
Your booking is confirmed!

If it is rejected, the output will be something like:

Javascript
                        
Error: No flights available
Your booking is cancelled.

Notice that the .then() method can return different types of values, such as strings, numbers, objects, or even other promises. If the returned value is a promise, the next handler will wait for it to settle, and use its value as the argument. If the returned value is not a promise, the next handler will use it as the argument directly.

The .catch() Method

The .catch() method takes one argument: a rejection handler, which is a function that will be called if the promise is rejected, or if any error occurs in the previous handlers. The .catch() method also returns a new promise, which can be chained with another .then() or .catch() method.

For example, the following code uses the flightPromise object and attaches a different handler to it:

Javascript
                        
// Use the flight promise
flightPromise
  // Attach a fulfillment handler
  .then((flight) => {
    // Log the flight value
    console.log(flight);
    // Throw an error
    throw new Error("Something went wrong");
  })
  // Attach a rejection handler
  .catch((error) => {
    // Log the error value
    console.error(error);
    // Return a cancellation message
    return "Your booking is cancelled.";
  })
  // Attach a final handler
  .then((message) => {
    // Log the message value
    console.log(message);
  });

The output of this code will be something like:

Javascript
                        
{ airline: 'AirBnB', price: '$199', duration: '3h 15m' }
Error: Something went wrong
Your booking is cancelled.

Notice that the .catch() method can handle both the rejection of the flightPromise object, and the error thrown by the first handler. The .catch() method can be used to handle any errors that may occur in the promise chain, and provide a fallback value or action.

Promise Methods

Besides the .then() and .catch() methods, there are some other methods that can be used with promises, such as .finally(), Promise.all(), Promise.race(), Promise.resolve(), and Promise.reject().

The .finally() Method

The .finally() method takes one argument: a final handler, which is a function that will be called after the promise is settled, regardless of its state. The .finally() method does not affect the value or the state of the promise, but it can be used to perform some cleanup or final actions. For example, the following code uses the .finally() method to log a message after the flightPromise object is settled:

Javascript
                        
// Use the flight promise
flightPromise
  // Attach a fulfillment handler
  .then((flight) => {
    // Log the flight value
    console.log(flight);
  })
  // Attach a rejection handler
  .catch((error) => {
    // Log the error value
    console.error(error);
  })
  // Attach a final handler
  .finally(() => {
    // Log a message
    console.log("The promise is settled.");
  });

The output of this code will be something like:

Javascript
                        
{ airline: 'AirBnB', price: '$199', duration: '3h 15m' }
The promise is settled.

Or:

Javascript
                        
Error: No flights available
The promise is settled.

The Promise.all() Method

The Promise.all() method takes an array of promises as an argument, and returns a new promise that will be fulfilled with an array of values, if all the promises in the array are fulfilled, or rejected with the first reason, if any of the promises in the array are rejected. The Promise.all() method can be used to wait for multiple promises to finish, and use their values together. For example, the following code uses the Promise.all() method to book a flight, a hotel, and a car:

Javascript
                        
// Create a function that returns a promise that simulates a flight search
function searchFlight() {
  return new Promise((resolve, reject) => {
    // Simulate a random delay
    let delay = Math.floor(Math.random() * 10000);

    // Simulate a random success or failure
    let success = Math.random() > 0.5;

    // Wait for the delay to finish
    setTimeout(() => {
      // If success, resolve the promise with the flight details
      if (success) {
        resolve({
          airline: "AirBnB",
          price: "$199",
          duration: "3h 15m",
        });
      }
      // If failure, reject the promise with an error
      else {
        reject(new Error("No flights available"));
      }
    }, delay);
  });
}

// Create a function that returns a promise that simulates a hotel search
function searchHotel() {
  return new Promise((resolve, reject) => {
    // Simulate a random delay
    let delay = Math.floor(Math.random() * 10000);

    // Simulate a random success or failure
    let success = Math.random() > 0.5;

    // Wait for the delay to finish
    setTimeout(() => {
      // If success, resolve the promise with the hotel details
      if (success) {
        resolve({
          name: "Hotel California",
          rating: "⭐⭐⭐⭐",
          price: "$99",
        });
      }
      // If failure, reject the promise with an error
      else {
        reject(new Error("No hotels available"));
      }
    }, delay);
  });
}

// Create a function that returns a promise that simulates a car rental
function rentCar() {
  return new Promise((resolve, reject) => {
    // Simulate a random delay
    let delay = Math.floor(Math.random() * 10000);

    // Simulate a random success or failure
    let success = Math.random() > 0.5;

    // Wait for the delay to finish
    setTimeout(() => {
      // If success, resolve the promise with the car details
      if (success) {
        resolve({
          model: "Tesla Model 3",
          color: "red",
          price: "$49",
        });
      }
      // If failure, reject the promise with an error
      else {
        reject(new Error("No cars available"));
      }
    }, delay);
  });
}

// Use the Promise.all() method to book a flight, a hotel, and a car
Promise.all([searchFlight(), searchHotel(), rentCar()])
  // Attach a fulfillment handler
  .then((values) => {
    // Log the values array
    console.log(values);
    // Return a confirmation message
    return "Your booking is confirmed!";
  })
  // Attach a rejection handler
  .catch((error) => {
    // Log the error value
    console.error(error);
    // Return a cancellation message
    return "Your booking is cancelled.";
  })
  // Attach a final handler
  .then((message) => {
    // Log the message value
    console.log(message);
  });

The output of this code will depend on the random success or failure of the promises in the array. If all of them are fulfilled, the output will be something like:

Javascript
                        
[
  { airline: 'AirBnB', price: '$199', duration: '3h 15m' },
  { name: 'Hotel California', rating: '⭐⭐⭐⭐', price: '$99' },
  { model: 'Tesla Model 3', color: 'red', price: '$49' }
]
Your booking is confirmed!

If any of them are rejected, the output will be something like:

Javascript
                        
Error: No hotels available
Your booking is cancelled.

Notice that the Promise.all() method will wait for the longest promise to finish, and use the order of the promises in the array to determine the order of the values in the result array.

The Promise.race() Method

The Promise.race() method takes an array of promises as an argument, and returns a new promise that will be fulfilled or rejected with the value or the reason of the first promise that settles. The Promise.race() method can be used to implement a timeout or a cancellation mechanism for promises. For example, the following code uses the Promise.race() method to cancel the flightPromise object if it takes too long:

Javascript
                        
// Create a function that returns a promise that rejects after a given time
function timeout(ms) {
  return new Promise((resolve, reject) => {
    // Wait for the given time to finish
    setTimeout(() => {
      // Reject the promise with an error
      reject(new Error("Timeout"));
    }, ms);
  });
}

// Use the Promise.race() method to cancel the flight promise if it takes too long
Promise.race([flightPromise, timeout(5000)])
  // Attach a fulfillment handler
  .then((flight) => {
    // Log the flight value
    console.log(flight);
    // Return a confirmation message
     "Your booking is confirmed!";
  })
  // Attach a rejection handler
  .catch((error) => {
    // Log the error value
    console.error(error);
    // Return a cancellation message
    return "Your booking is cancelled.";
  })
  // Attach a final handler
  .then((message) => {
    // Log the message value
    console.log(message);
  });

The output of this code will depend on the random delay of the flightPromise object. If it is less than 5000 milliseconds, the output will be something like:

Javascript
                        
{ airline: 'AirBnB', price: '$199', duration: '3h 15m' }
Your booking is confirmed!

If it is more than 5000 milliseconds, the output will be something like:

Javascript
                        
Error: Timeout
Your booking is cancelled.

Notice that the Promise.race() method will ignore the other promises in the array, once the first promise settles.

The Promise.resolve() Method

The Promise.resolve() method takes a value as an argument, and returns a new promise that is fulfilled with that value. The Promise.resolve() method can be used to create a promise from any value, or to convert a thenable object (an object with a .then() method) to a promise. For example, the following code uses the Promise.resolve() method to create a promise from a string:

Javascript
                        
// Use the Promise.resolve() method to create a promise from a string
Promise.resolve("Hello, world!")
  // Attach a fulfillment handler
  .then((value) => {
    // Log the value
    console.log(value);
  });

The output of this code will be:

Javascript
                        
Hello, world!
                        
                    

The Promise.reject() Method

The Promise.reject() method takes a reason as an argument, and returns a new promise that is rejected with that reason. The Promise.reject() method can be used to create a promise that is already rejected, or to reject a promise with a custom reason. For example, the following code uses the Promise.reject() method to create a promise that is rejected with an error:

Javascript
                        
// Use the Promise.reject() method to create a promise that is rejected with an error
Promise.reject(new Error("Something went wrong"))
  // Attach a rejection handler
  .catch((error) => {
    // Log the error
    console.error(error);
  });

The output of this code will be:

Javascript
                        
Error: Something went wrong
                        
                    

Advantages of Promises

Promises have several advantages over callbacks and events for handling asynchronous operations. Some of the main advantages are:

  • Promises are easy to read and write, as they use a chainable syntax that avoids the callback hell problem, where nested callbacks make the code hard to follow and maintain.
  • Promises are easy to compose and reuse, as they can be returned from functions, passed as arguments, stored in variables, or combined with other promises using methods like Promise.all() or Promise.race().
  • Promises are easy to handle errors, as they can use a single .catch() method to handle any errors that may occur in the promise chain, instead of using multiple error callbacks or event listeners.
  • Promises are consistent and predictable, as they follow a well-defined specification that ensures they behave in the same way across different platforms and libraries.

Best Practices for Promises

Promises are powerful and flexible tools for asynchronous programming, but they also require some care and attention to use them effectively and avoid common pitfalls. Some of the best practices for promises are:

  • Always return a promise from a .then() or .catch() handler, unless you want to end the chain. This will allow you to chain more handlers, or handle errors properly.
  • Always handle errors or rejections, either by using a .catch() method at the end of the chain, or by using a second argument in the .then() method. This will prevent unhandled promise rejections, which can cause memory leaks or silent failures.
  • Avoid creating unnecessary promises, such as wrapping a synchronous value or function in a promise. This will improve the performance and readability of your code.
  • Avoid mutating or modifying the values or the state of the promises, such as changing the value of a resolved promise, or calling resolve or reject multiple times. This will ensure the integrity and consistency of your promises.
  • Use descriptive and meaningful names for your promises and their values, such as flightPromise or flight, instead of p or x. This will make your code more understandable and maintainable.

Conclusion

Promises are a powerful and elegant way to handle asynchronous operations in JavaScript. They allow us to write clean, readable, and modular code that can handle multiple tasks at the same time, without blocking the main thread of execution. Promises also make it easy to handle errors and failures, and to compose and reuse different functions that return promises. By following the promise specification and some best practices, we can ensure that our promises are consistent, predictable, and reliable. Promises are not only a feature of the JavaScript language, but also a paradigm of programming that can help us create better and more efficient applications.