JavaScript Promise Tutorial: Resolve, Reject, and Chaining in JS and ES6

Promises are a fundamental concept in JavaScript that help us write cleaner, more readable asynchronous code. They provide a structured way to handle operations that take an unknown amount of time, such as requesting data from a server or reading files from disk. Promises have become an essential tool for JavaScript developers, especially with the introduction of async/await in ES6.

In this tutorial, we‘ll take an in-depth look at JavaScript promises, including how to create them, handle fulfillment and rejection, and chain promises together. We‘ll start with the basics and work our way up to more advanced topics. Whether you‘re new to promises or just need a refresher, this guide will give you a solid understanding of how they work and how to use them effectively in your code.

What are Promises?

A promise is an object that represents the eventual completion or failure of an asynchronous operation and its resulting value. In other words, a promise is a placeholder for a value that may not be available yet but will be resolved at some point in the future.

Promises have three possible states:

  1. Pending: initial state, neither fulfilled nor rejected.
  2. Fulfilled: the operation completed successfully.
  3. Rejected: the operation failed.

A pending promise can either be fulfilled with a value or rejected with a reason (error). Once a promise is fulfilled or rejected, it is immutable (i.e. it can never change states again).

Here‘s a simple example of what a promise looks like:

let myPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(‘Success!‘);
  }, 1000);
});

In this example, we create a new promise that will be resolved with the string ‘Success!‘ after 1 second. The setTimeout() is used to simulate an asynchronous operation.

Why Use Promises?

Promises provide a cleaner and more readable alternative to traditional callback functions. With callbacks, it‘s easy to get into "callback hell", where you have to nest multiple callbacks within each other, leading to code that is difficult to read and maintain.

For example, consider the following code that uses callbacks to read a file and then compress it:

fs.readFile(‘file.txt‘, (err, data) => {
  if (err) {
    console.error(err);
  } else {
    zlib.zip(data, (err, buffer) => { 
      if (err) {
        console.error(err);
      } else {
        fs.writeFile(‘file.zip‘, buffer, (err) => {
          if (err) {
            console.error(err);
          } else {
            console.log(‘File successfully compressed‘);
          }
        });
      }
    });
  }
});

As you can see, the nesting of callbacks can quickly get out of hand, even for a simple operation like this. Promises allow us to write this same code in a much more straightforward and readable way:

fs.promises.readFile(‘file.txt‘)
  .then(data => {
    return zlib.promises.zip(data);
  })
  .then(buffer => { 
    return fs.promises.writeFile(‘file.zip‘, buffer);
  })
  .then(() => {
    console.log(‘File successfully compressed‘);  
  })
  .catch(err => {
    console.error(err);
  });

With promises, we can chain multiple asynchronous operations together using .then(), making the code much easier to follow. We also have a catch() method that allows us to handle any errors that might occur along the way.

Creating Promises

You can create a new promise using the Promise constructor, which takes a single argument: a callback function, sometimes referred to as the "executor function". This function takes two arguments: resolve and reject.

const myPromise = new Promise((resolve, reject) => {
  // do something async 
  // call resolve(value) when done
  // call reject(reason) if error  
});  

Inside the executor function, you perform your asynchronous operation. If it completes successfully, you call resolve(value), where value is the fulfilled value of the promise. If an error occurs, you call reject(reason), where reason is typically an Error object indicating why the promise was rejected.

Here‘s an example that demonstrates creating a promise that will be resolved after a certain amount of time has passed:

const timeoutPromise = (timeout) => new Promise((resolve) => {
  setTimeout(() => {
    resolve(`Resolved after ${timeout}ms`);
  }, timeout);
});

In this example, the promise is resolved with a string indicating how long it took to resolve. We can use this promise like so:

timeoutPromise(1000)
  .then(value => {  
    console.log(value); // Resolved after 1000ms
  });

Handling Fulfilled and Rejected Promises

Once a promise is resolved, any handlers attached to it with .then() will be called with the fulfilled value. If the promise is rejected, any handlers attached with .catch() (or the second argument to .then()) will be called with the reason for rejection.

myPromise
  .then(value => {
    console.log(value); // fulfillment value  
  })
  .catch(reason => {
    console.error(reason); // rejection reason
  });  

You can attach multiple handlers to a single promise, and they will be called in the order they were attached. Each handler receives the value returned by the previous handler.

myPromise
  .then(value => {
    console.log(value); // ‘Hello‘
    return value + ‘ World‘; 
  })
  .then(value => {
    console.log(value); // ‘Hello World‘  
  });

If a handler returns a promise, the next handler waits for it to settle before being called with its value.

Promise Chaining

One of the most powerful features of promises is the ability to chain them together. When you return a value from a .then() handler, it will be wrapped in a promise and passed to the next .then() handler. If you return a promise from a .then() handler, the next handler waits for that promise to settle before being called.

Here‘s an example that demonstrates chaining promises together to fetch some JSON data from an API:

fetch(‘https://api.example.com/data‘)
  .then(response => response.json())
  .then(data => {
    console.log(data);
    return fetch(‘https://api.example.com/more-data‘); 
  })
  .then(response => response.json())
  .then(data => {
    console.log(data);
  })
  .catch(error => {
    console.error(‘Error:‘, error);  
  });

In this example, we first fetch some JSON data from an API endpoint. The response is passed to the first .then() handler, which parses the JSON using response.json(). This returns a promise that resolves with the parsed data.

We log the data and then return a new promise from the first .then() handler by calling fetch() again to get more data from a different API endpoint. The second .then() handler receives the response from this new request, parses the JSON, and logs the second set of data.

Finally, we have a .catch() handler at the end to log any errors that might have occurred along the way. This is a common pattern in promise chains – having a single .catch() at the end to handle errors in any of the .then() handlers.

Promise.all() and Promise.race()

Sometimes you need to work with multiple promises at once. The Promise.all() and Promise.race() methods allow you to do this.

Promise.all() takes an array of promises and returns a new promise that fulfills when all the promises in the array have fulfilled, or rejects if any of the promises reject. The fulfilled value is an array of the fulfillment values of the individual promises, in the same order.

const promise1 = Promise.resolve(1);
const promise2 = Promise.resolve(2);
const promise3 = Promise.resolve(3);

Promise.all([promise1, promise2, promise3]) .then(values => { console.log(values); // [1, 2, 3] });

Promise.race() also takes an array of promises, but it fulfills or rejects as soon as one of the promises in the array fulfills or rejects. The fulfilled value or rejection reason is the value or reason from that first settled promise.

const promise1 = new Promise((resolve, reject) => {
  setTimeout(resolve, 500, ‘first‘); 
});

const promise2 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, ‘second‘); });

Promise.race([promise1, promise2]) .then(value => { console.log(value); // ‘second‘
});

In this example, even though promise1 is settled first, promise2 is fulfilled faster so the fulfilled value is ‘second‘.

Async/Await

Async/await is a newer syntax built on top of promises that makes asynchronous code even easier to write and reason about. An async function always returns a promise, and the await keyword can be used inside an async function to pause execution until a promise settles.

Here‘s an example of using async/await to fetch data from an API:

async function getData() {
  try {
    const response = await fetch(‘https://api.example.com/data‘);
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error(‘Error:‘, error);
  }
}

getData();

The await keyword pauses the execution of the getData function until the promise returned by fetch() settles. If the promise fulfills, execution resumes and the fulfilled value is assigned to the response variable. If the promise rejects, the rejection reason is thrown.

We use a try/catch block to handle any errors that might occur. If an error is thrown (either by an explicit throw or by a rejected promise), execution jumps to the catch block.

Async/await makes promise-based code read more like synchronous code, which can make it easier to understand and reason about. However, it‘s important to remember that async/await is still built on promises under the hood.

Conclusion

Promises are a powerful feature of JavaScript that allow you to write cleaner, more readable asynchronous code. They provide a way to structure and reason about operations that may not complete immediately, such as network requests or disk I/O.

In this tutorial, we‘ve covered the basics of creating, resolving, and rejecting promises, as well as how to handle promise fulfillment and rejection with .then() and .catch(). We‘ve also looked at more advanced topics like promise chaining, working with multiple promises using Promise.all() and Promise.race(), and using the newer async/await syntax.

With a solid understanding of promises under your belt, you‘re well-equipped to write modern, asynchronous JavaScript code. Promises are used extensively in Node.js and in front-end frameworks like Angular, Vue, and React, so mastering them is an essential skill for any JavaScript developer.

Similar Posts