JavaScript Promises: What Are They?

Updated on Jan 20, 2024


Table of Contents


What are promises?

A promise is an object that represents the eventual completion or failure of an asynchronous operation. A promise can be in one of three states:

  • Pending: The initial state, before the promise is settled.
  • Fulfilled: The state when the promise is resolved with a value.
  • Rejected: The state when the promise is rejected with a reason.

A promise can only be settled once, and it cannot change its state after that. A promise can be settled either by calling its resolve function with a value, or by calling its reject function with a reason. Once a promise is settled, it will trigger the corresponding handler function that was attached to it.

How to create a promise?

To create a promise, you can use the Promise constructor, which takes a function as an argument. This function is called the executor, and it receives two parameters: resolve and reject. The executor is responsible for starting the asynchronous operation and calling either resolve or reject when it is done.

For example, let's create a promise that simulates a network request:

// Create a promise that resolves with "Hello, world!" after 2 seconds
const promise = new Promise((resolve, reject) => {
  // Start the asynchronous operation
  setTimeout(() => {
    // Call resolve with the value
    resolve("Hello, world!");
  }, 2000);
});

How to use a promise?

To use a promise, you can attach handler functions to it using the then and catch methods. The then method takes two arguments: a function to handle the fulfillment, and a function to handle the rejection. The catch method takes one argument: a function to handle the rejection. Both methods return a new promise, which can be chained further.

For example, let’s use the promise we created earlier:

// Use the promise
promise
  // Handle the fulfillment
  .then((value) => {
    // Log the value
    console.log(value); // "Hello, world!"
  })
  // Handle the rejection
  .catch((reason) => {
    // Log the reason
    console.error(reason); // This will not run in this example
  });

Promise chaining

One of the benefits of promises is that they allow you to chain multiple asynchronous operations together. This means that you can return another promise from a then or catch handler, and it will be resolved or rejected before the next handler is called. This way, you can avoid nesting callbacks and write more linear code.

For example, let’s chain three promises together:

// Create three promises that resolve with different values
const promise1 = Promise.resolve("First");
const promise2 = Promise.resolve("Second");
const promise3 = Promise.resolve("Third");

// Chain the promises
promise1
  // Handle the first promise
  .then((value) => {
    // Log the value
    console.log(value); // "First"
    // Return the second promise
    return promise2;
  })
  // Handle the second promise
  .then((value) => {
    // Log the value
    console.log(value); // "Second"
    // Return the third promise
    return promise3;
  })
  // Handle the third promise
  .then((value) => {
    // Log the value
    console.log(value); // "Third"
  });

Promise.all and Promise.race

Sometimes, you may want to perform multiple asynchronous operations in parallel, and wait for them all to finish. For this, you can use the Promise.all method, which takes an array of promises as an argument, and returns a new promise that resolves with an array of the values of the input promises, or rejects with the reason of the first rejected promise.

For example, let’s use Promise.all to wait for three promises to resolve:

// Create three promises that resolve with different values
const promise1 = Promise.resolve("First");
const promise2 = Promise.resolve("Second");
const promise3 = Promise.resolve("Third");

// Use Promise.all to wait for all promises to resolve
Promise.all([promise1, promise2, promise3])
  // Handle the fulfillment
  .then((values) => {
    // Log the values
    console.log(values); // ["First", "Second", "Third"]
  })
  // Handle the rejection
  .catch((reason) => {
    // Log the reason
    console.error(reason); // This will not run in this example
  });

Other times, you may want to perform multiple asynchronous operations in parallel, and only care about the first one to finish. For this, you can use the Promise.race method, which takes an array of promises as an argument, and returns a new promise that resolves or rejects with the value or reason of the first settled promise.

For example, let’s use Promise.race to get the fastest response from two network requests:

// Create two promises that simulate network requests
const promise1 = new Promise((resolve, reject) => {
  // Resolve with "First" after 1 second
  setTimeout(() => {
    resolve("First");
  }, 1000);
});
const promise2 = new Promise((resolve, reject) => {
  // Resolve with "Second" after 2 seconds
  setTimeout(() => {
    resolve("Second");
  }, 2000);
});

// Use Promise.race to get the fastest response
Promise.race([promise1, promise2])
  // Handle the fulfillment
  .then((value) => {
    // Log the value
    console.log(value); // "First"
  })
  // Handle the rejection
  .catch((reason) => {
    // Log the reason
    console.error(reason); // This will not run in this example
  });

Error handling

One of the challenges of working with asynchronous code is handling errors. Promises make this easier by providing a consistent way to catch and handle errors. There are two ways to reject a promise: by calling the reject function in the executor, or by throwing an error in the executor or any of the handler functions. Either way, the promise will be rejected with the reason, and the catch handler will be called.

For example, let’s create and use a promise that rejects with an error:

// Create a promise that rejects with an error
const promise = new Promise((resolve, reject) => {
  // Throw an error
  throw new Error("Something went wrong!");
});

// Use the promise
promise
  // Handle the fulfillment
  .then((value) => {
    // This will not run in this example
  })
  // Handle the rejection
  .catch((reason) => {
    // Log the reason
    console.error(reason); // Error: Something went wrong!
  });

Note: If you don’t attach a catch handler to a rejected promise, the error will be silently ignored, unless you are using a tool that warns you about unhandled promise rejections. This can lead to hard-to-debug issues, so it is recommended to always use a catch handler, or use the finally handler to handle both fulfillment and rejection.

Async/await syntax

Another way to work with promises is to use the async and await keywords, which are part of the ES2017 standard. The async keyword allows you to declare a function that returns a promise, and the await keyword allows you to pause the execution of the function until a promise is settled. This way, you can write asynchronous code that looks like synchronous code.

For example, let’s rewrite the promise chaining example using async and await:

// Create three promises that resolve with different values
const promise1 = Promise.resolve("First");
const promise2 = Promise.resolve("Second");
const promise3 = Promise.resolve("Third");

// Declare an async function
async function asyncFunction() {
  // Use await to wait for the first promise
  const value1 = await promise1;
  // Log the value
  console.log(value1); //
  // "First"
  // Use await to wait for the second promise
  const value2 = await promise2;
  // Log the value
  console.log(value2); // "Second"
  // Use await to wait for the third promise
  const value3 = await promise3;
  // Log the value
  console.log(value3); // "Third"
}

// Call the async function
asyncFunction();

Note: The await keyword can only be used inside an async function, otherwise it will cause a syntax error. Also, the async and await keywords do not block the main thread, they are just syntactic sugar for promises.

Conclusion

In this blog post, I have explained what JavaScript promises are, how to create and use them, and some common patterns and pitfalls to avoid. I have also shown how to use the async and await syntax to write asynchronous code that looks like synchronous code. Promises are a powerful way to handle asynchronous operations in your code, and they can make your code more clean and readable. I hope you have learned something new and useful from this blog post, and I encourage you to practice and experiment with promises in your own projects.

How did you like the article?

like

Thanks for reading! 🙏