Promises in JavaScript

You've learned how asynchronous behavior is crucial for handling operations like fetching data, reading files, or waiting for user actions without blocking the main thread. Now, let's dive deeper into more advanced asynchronous programming concepts, focusing on promises.

What Are Promises?

Promises are a more sophisticated way to handle asynchronous operations in JavaScript, providing a cleaner, more manageable alternative to callbacks.

A promise represents an asynchronous operation's eventual completion (or failure) and its resulting value.

It's like ordering a pizza online: you place the order (the async operation starts) and are promised a delicious meal. It might arrive on time (success), be delayed (pending), or not show up at all (failure).

The States of a Promise

A promise in JavaScript can be in one of three states:

  1. Pending: The initial state, meaning the promise is neither fulfilled nor rejected—it's still waiting for something to happen.
  2. Fulfilled: The operation completed successfully, and the promise now has a value (like your pizza arriving hot and ready to eat).
  3. Rejected: The operation failed, and the promise has a reason for the failure (like the pizza place telling you they're out of your favorite toppings).

Creating and Using Promises

Let's start with a simple example of creating and using a promise:

// Creating a new Promise and assigning it to a variable
const myPromise = new Promise((resolve, reject) => {
    const isSuccessful = true; // Mocking an operation's success or failure

    if (isSuccessful) {
        resolve("The operation was successful!");
    } else {
        reject("The operation failed.");
    }
});

myPromise
    .then((message) => {
        console.log(message); // This runs if the promise is fulfilled
    })
    .catch((error) => {
        console.log(error); // This runs if the promise is rejected
    });

Breaking it down

  1. Creating a Promise: We use new Promise to create a promise. Inside, there's an executor function that takes two arguments: resolve and reject. These are like the "yes" and "no" buttons. If everything goes well, resolve is called with a success message. If something goes wrong, reject is called with an error message.

  2. Using the Promise: After creating the promise, we attach handlers to it using .then() for success and .catch() for errors. The .then() method gets executed when the promise is fulfilled, and .catch() runs if the promise is rejected.

This pattern helps predict the results of asynchronous operations.

Chaining Promises: Building on Results

One of the most powerful features of promises is the ability to chain them together, passing the result of one promise to the next. This is particularly useful when you need to perform a sequence of asynchronous operations that depend on each other. Let's expand on our previous example to illustrate this concept:

let myPromise = new Promise((resolve, reject) => {
    let isSuccessful = true; // Simulating an operation's success or failure

    if (isSuccessful) {
        resolve("First operation successful!");
    } else {
        reject("First operation failed.");
    }
});

myPromise
    .then((message) => {
        console.log(message);
        // Use the result from the first promise to initiate the second promise
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                let secondResult = `${message} Now, second operation completed after 2 seconds!`;
                resolve(secondResult);
            }, 2000);
        });
    })
    .then((secondMessage) => {
        console.log(secondMessage);
        // Using the result from the second promise in a third operation
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                let finalResult = `${secondMessage} Finally, the third operation is done!`;
                resolve(finalResult);
            }, 2000);
        });
    })
    .then((finalMessage) => {
        console.log(finalMessage);
    })
    .catch((error) => {
        console.log(error);
    });

What's happening here?

  1. First Promise (myPromise): We start with a basic promise that resolves or rejects based on a condition. If the promise resolves, it passes the message "First operation successful!" to the next .then() block.

  2. First .then(): The message from the resolved promise is logged and then used to create a new promise. This new promise simulates another async operation using setTimeout, which resolves after 2 seconds with a new message that includes the result from the first promise.

  3. Second .then(): The result from the second promise is logged and used to create yet another promise. This promise also uses setTimeout to simulate a delay, then resolves with a final message that combines all the previous results.

  4. Third .then(): This logs the final message, showing the complete sequence of operations.

  5. .catch(): If any promise in the chain is rejected, the .catch() block will handle the error, providing a way to handle failures in the sequence of operations.

Promises, especially when chained together, offer a simple way to handle complex sequences of asynchronous operations. They help you manage dependencies between operations, keep your code clean and readable, and provide a straightforward way to handle errors.

As you practice with promises, you'll see just how versatile and essential they are in modern JavaScript development.

Next, we'll explore async and await, which build on promises and allow you to write asynchronous code that looks and behaves more like synchronous code.

BeginnerJavaScript
Avatar for Niall Maher

Written by Niall Maher

Founder of Codú - The web developer community! I've worked in nearly every corner of technology businesses: Lead Developer, Software Architect, Product Manager, CTO, and now happily a Founder.

Loading

Fetching comments

Hey! 👋

Got something to say?

or to leave a comment.