JavaScript Asynchronous Operations and Promises: in Depth

JavaScript Asynchronous Operations and Promises: in Depth

Table of Contents

  • Introduction

  • Promises: A Better Way to Write Asynchronous JavaScript

  • Create a Promise Chain in JavaScript with Promise.prototype.then()

  • Catch Errors in a JavaScript Promise Chain with Promise.prototype.catch()

  • Execute Cleanup Logic in a JavaScript Promise Chain with Promise.prototype.finally()

  • Create a Rejected Promise in JavaScript with Promise.reject()

  • Create a New Promise in JavaScript with the Promise Constructor

  • Wait for the Fastest JavaScript Promise to Settle with Promise.race()

  • Wait for Multiple JavaScript Promises to Be Fulfilled with Promise.all()

  • Wait for Multiple JavaScript Promises to Settle withPromise.allSettled()

  • Wait for the Fastest JavaScript Promise to Be Fulfilled with Promise.any()

  • Await a JavaScript Promise in an async Function with the await Operator

  • Summary

Introduction

JavaScript is a language that allows us to perform tasks that take some time to complete, such as fetching data from an API, reading files, or writing to a database. These tasks are called asynchronous operations, because they do not block the execution of the rest of the code while they are in progress.

Promises: A Better Way to Write Asynchronous JavaScript

The Pizza Bazaar Analogy :

To understand how asynchronous operations work in JavaScript, let’s use an analogy of a pizza bazaar. Imagine that you go to a pizza bazaar and order a pizza. The pizza maker will take your order and start preparing it, but he will not make you wait until it is ready. Instead, he will give you a ticket with a number on it, and tell you to come back later when your pizza is done. This way, you can do other things while your pizza is being cooked, such as browsing other stalls, buying drinks, or chatting with friends.

The ticket that the pizza maker gives you is like a promise in JavaScript. A promise is an object that represents the eventual result of an asynchronous operation. It has three possible states :

  • Pending: The promise is not yet fulfilled or rejected. This is the initial state of a promise.

  • Fulfilled: The promise is successfully resolved with a value. This means that the asynchronous operation has completed and returned a result.

  • Rejected: The promise is rejected with a reason. This means that the asynchronous operation has failed or thrown an error.

What problems do promises solve?

Promises solve a number of problems with asynchronous programming in JavaScript:

  • Callback hell: Promises can help us to avoid callback hell, which is a situation where we have a chain of nested callbacks that can be difficult to read and debug.

  • Error handling: Promises make it easier to handle errors in asynchronous code. We can use the .catch() method to handle errors in a central location, rather than having to pass error callbacks to every function in the chain.

  • Readability and maintainability: Promises make asynchronous code more readable and maintainable. We can use the .then() method to chain together asynchronous operations in a linear way.

Create a Promise Chain in JavaScript with Promise.prototype.then()

One of the methods that we can use on promises is then(). The then() method takes two arguments: a success callback and an error callback. The success callback is a function that will be called if the promise is fulfilled, and it will receive the value of the promise as its parameter. The error callback is a function that will be called if the promise is rejected, and it will receive the reason of the rejection as its parameter.

For example, if we want to get the data from the response object that we got from the fetch function, we can write something like this:

 promise = fetch('https://example.com/api/data');

promise.then(
  // success callback
  function(response) {
    // do something with the response object
  },
  // error callback
  function(error) {
    // do something with the error
  }
);

The then() method also returns another promise, which can be chained with another then()method. This way, we can perform multiple asynchronous operations in sequence and handle each result separately.

Catch Errors in a JavaScript Promise Chain with Promise.prototype.catch()

However, what if something goes wrong in one of the steps of our promise chain? For example, what if the response object is not valid JSON, or what if there is an error in our console.log statement? In that case, our promise chain will be broken, and we will not be able to handle the error properly.

To solve this problem, we can use another method on promises called catch(). The catch() method takes one argument: an error callback. The error callback is a function that will be called if any of the promises in the chain are rejected, and it will receive the reason of the rejection as its parameter.

For example, if we want to handle any errors that occur in our promise chain, we can write something like this:

promise = fetch('https://example.com/api/data');

promise.then(
  // success callback
  function(response) {
    // return another promise that parses the response as JSON
    return response.json();
  }
).then(
  // success callback
  function(data) {
    // do something with the data
    console.log(data);
  }
).catch(
  // error callback
  function(error) {
    // do something with the error
    console.error(error);
  }
);

The catch method will catch any errors that occur in the promise chain, such as network failures, invalid responses, or syntax errors. This way, we can handle all possible outcomes of our asynchronous operations in a single place.

Execute Cleanup Logic in a JavaScript Promise Chain with Promise.prototype.finally()

Sometimes, we may want to perform some actions regardless of whether our promise chain is fulfilled or rejected. For example, we may want to close some resources, display some messages, or update some UI elements. To do this, we can use another method on promises called finally(). The finally() method takes one argument: a callback that runs regardless of the promise outcome. The callback does not receive any parameters, and it does not affect the value or the state of the promise.

For example, if we want to perform some cleanup actions after our promise chain, we can write something like this:

promise = fetch('https://example.com/api/data');

promise.then(
  // success callback
  function(response) {
    // return another promise that parses the response as JSON
    return response.json();
  }
).then(
  // success callback
  function(data) {
    // do something with the data
    console.log(data);
  }
).catch(
  // error callback
  function(error) {
    // do something with the error
    console.error(error);
  }
).finally(
  // callback that runs regardless of the promise outcome
  function() {
    // do some cleanup actions
    console.log('Done');
  }
);

The finally() method will run after the promise chain is either fulfilled or rejected, and it will not affect the value or the state of the promise. This way, we can execute some cleanup logic without having to repeat it in both the success and the error callbacks.

Create a Rejected Promise in JavaScript with Promise.reject()

So far, we have seen how to create promises using the fetch function and how to handle them using the then(), catch(), and finally() methods. However, there are other ways to create promises in JavaScript. One of them is to use the Promise.reject function, which creates a promise that is immediately rejected with a given reason.

For example, if we want to create a rejected promise with a custom error message, we can write something like this:

promise = Promise.reject('Something went wrong');

promise.then(
  // success callback
  function(value) {
    // this will not run
    console.log(value);
  }
).catch(
  // error callback
  function(reason) {
    // this will run
    console.error(reason); // Something went wrong
  }
);

The Promise.reject function is useful when we want to simulate an error condition or when we want to reject a promise based on some logic. For example, if we want to reject a promise if the response status is not OK, we can write something like this:

promise = fetch('https://example.com/api/data');

promise.then(
  // success callback
  function(response) {
    // check the response status
    if (response.ok) {
      // return another promise that parses the response as JSON
      return response.json();
    } else {
      // reject the promise with the status text
      return Promise.reject(response.statusText);
    }
  }
).then(
  // success callback
  function(data) {
    // do something with the data
    console.log(data);
  }
).catch(
  // error callback
  function(error) {
    // do something with the error
    console.error(error); // Not Found, Internal Server Error, etc.
  }
);

The Promise.reject function allows us to create a rejected promise with any value that we want and pass it to the next catch method in the chain.

Create a Resolved Promise in JavaScript with Promise.resolve()

In the previous section, we learned how to create a rejected promise using the Promise.reject function. In this section, we will learn how to create a resolved promise using the Promise.resolve function, which creates a promise that is immediately resolved with a given value.

For example, if we want to create a resolved promise with a custom message, we can write something like this:

 // Creating a resolved promise with a custom message
const customMessagePromise = Promise.resolve('Everything is fine');

customMessagePromise
  .then((value) => {
    // Success callback function
    console.log(value); // Output: Everything is fine
  })
  .catch((reason) => {
    // Error callback function
    console.error(reason); // This will not run
  });

ThePromise.resolve function is useful when we want to simulate a success condition or when we want to resolve a promise based on some logic. For example, if we want to resolve a promise if the response status is OK, we can write something like this:

// Resolving a promise based on the response status
const fetchDataPromise = fetch('https://example.com/api/data');

fetchDataPromise
  .then((response) => {
    // Success callback function
    // Check the response status
    if (response.ok) {
      // Resolve the promise with the response as JSON
      return response.json();
    } else {
      // Reject the promise with the status text
      return Promise.reject(response.statusText);
    }
  })
  .then((data) => {
    // Success callback function
    // Do something with the data
    console.log(data);
  })
  .catch((error) => {
    // Error callback function
    // Do something with the error
    console.error(error); // Possible outputs: 'Not Found', 'Internal Server Error', etc.
  });

The Promise.resolve function allows us to create a resolved promise with any value that we want and pass it to the next then() or catch() method in the chain. This way, we can control the flow of our promise-based code and handle different scenarios accordingly.

Create a New Promise in JavaScript with the Promise Constructor

The Promise constructor is used to create a new Promise object. It receives a single function as a parameter (known as the executor function), which in turn receives the resolve and reject functions as parameters:

const promise = new Promise((resolve, reject) => {
  // Perform some operation, then call either resolve() or reject()
});

Within the body of the executor function, you can perform any operation — typically, an asynchronous one. You then either call resolve(value) or reject(reason), depending on the outcome of that operation, to fulfill or reject the promise.

Note that the Promise object is rejected if an error is thrown within the body of the executor function. The return value of the executor function is ignored.

For Second example, if we want to create a promise that simulates a delay using the setTimeout function, we can write something like this:

// Create a function that returns a promise that resolves after a given time
function sleep(ms) {
  // Return a new promise using the Promise constructor
  return new Promise(resolve => {
    // Use setTimeout to call resolve after ms milliseconds
    setTimeout(resolve, ms);
  });
}

// Call the sleep function with 1000 milliseconds
sleep(1000)
  .then(() => {
    // This will run after 1 second
    console.log("After 1s");
  })
  .then(() => {
    // Return another promise that resolves after another second
    return sleep(1000);
  })
  .then(() => {
    // This will run after 2 seconds
    console.log("After 2s");
  })
  .catch(() => {
    // This will run if the promise is rejected
    console.log("Rejected");
  });

// This will run right away
console.log("Right away");

The Promise constructor is useful when we want to create a promise from scratch or when we want to wrap an existing asynchronous operation into a promise.
For example, if we want to create a promise that reads a file using the fs module, we can write something like this:

// Import the fs module
const fs = require('fs');

// Create a function that returns a promise that reads a file
function readFile(filename) {
  // Return a new promise using the Promise constructor
  return new Promise((resolve, reject) => {
    // Use fs.readFile to read the file
    fs.readFile(filename, 'utf8', (err, data) => {
      // If there is an error, reject the promise with the error
      if (err) {
        reject(err);
      } else {
        // Otherwise, resolve the promise with the data
        resolve(data);
      }
    });
  });
}

// Call the readFile function with 'test.txt'
readFile('test.txt')
  .then(data => {
    // This will run if the file is read successfully
    console.log(data);
  })
  .catch(err => {
    // This will run if there is an error reading the file
    console.error(err);
  });

Wait for the Fastest JavaScript Promise to Settle with Promise.race()

Sometimes, we may want to execute multiple asynchronous operations in parallel and get the result of the fastest one. For example, we may want to fetch data from different sources and use the first response that arrives, or we may want to set a timeout for a long-running operation and cancel it if it takes too long. In these cases, we can use the Promise.race() method to race multiple promises against each other and find the first promise to settle.

The Promise.race() method accepts an array (or any other iterable) of promises as a parameter. It returns a Promise object that is fulfilled or rejected once the first input promise is fulfilled or rejected:

  • As soon as any input promise is fulfilled, the returned Promise object is fulfilled with that value.

  • As soon as any input promise is rejected, the returned Promise object is rejected with that reason.

For example, let’s say we have a function that returns a promise that resolves after a given time with a given value:

function resolveAfter(ms, value) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(value);
    }, ms);
  });
}

We can use Promise.race() to get the value of the fastest promise among three promises:

Promise.race([
  resolveAfter(1000, "A"),
  resolveAfter(2000, "B"),
  resolveAfter(3000, "C")
]).then(value => {
  console.log(value); // A
});

The returned promise will be fulfilled with “A” after 1000 milliseconds, because that is the fastest promise to resolve.

We can also use Promise.race() to implement a timeout function that rejects a promise if it takes longer than a specified time. For example, we can write a function that takes a time limit and a promise as parameters, and returns a new promise that races the input promise with a timeout promise:

function timeout(ms, promise) {
  let timeoutID;
  const timeoutPromise = new Promise((_, reject) => {
    timeoutID = setTimeout(() => {
      reject(
        new Error(`Operation timed out after ${ms}ms`)
      );
    }, ms);
  });
  return Promise.race([promise, timeoutPromise]).finally(
    () => {
      clearTimeout(timeoutID);
    }
  );
}

The timeout function creates a timeoutPromise that rejects after ms milliseconds with an error message. It then returns a promise that races the input promise with the timeoutPromise. If the input promise settles before the timeoutPromise, the returned promise will settle with the same value or reason as the input promise. If the timeoutPromise settles before the input promise, the returned promise will be rejected with the error message. In either case, the finally() method will clear the timeoutID to avoid memory leaks.

We can use the timeout function to limit the execution time of any promise. For example, we can use it with our resolveAfter function to create a promise that resolves after 1000 milliseconds with “A”, and race it with a timeout of 500 milliseconds:

const promise = resolveAfter(1000, "A");

timeout(500, promise).then(
  value => {
    console.log(`Fulfilled: ${value}`);
  },
  error => {
    console.log(`Rejected: ${error}`);
  }
);

The returned promise will be rejected with an error message after 500 milliseconds, because the input promise takes longer than that to resolve.

Wait for Multiple JavaScript Promises to Be Fulfilled with Promise.all()

Sometimes, we may want to execute multiple asynchronous operations in parallel and get the results of all of them. For example, we may want to fetch data from different sources and combine them, or we may want to perform some calculations on different inputs and aggregate them. In these cases, we can use the Promise.all() method to wait for multiple promises to be fulfilled and get their values.

The Promise.all() method accepts an array (or any other iterable) of promises as a parameter. It returns a Promise object that is fulfilled if all of the input promises are fulfilled or rejected if any of the input promises is rejected:

  • If all input promises are fulfilled, the returned Promise object is fulfilled with an array of fulfillment values of all promises (in the same order as the promises passed to Promise.all()).

  • If any input promise is rejected, the returned Promise object is rejected with that reason.

For example, let’s say we have a function that returns a promise that resolves after a given time with a given value:

function resolveAfter(ms, value) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(value);
    }, ms);
  });
}

We can use Promise.all() to get the values of three promises that resolve after different times:

Promise.all([
  resolveAfter(1000, "A"),
  resolveAfter(2000, "B"),
  resolveAfter(3000, "C")
]).then(values => {
  console.log(values); // ["A", "B", "C"]
});

The returned promise will be fulfilled with an array of values [“A”, “B”, “C”] after 3000 milliseconds, because that is the longest time among the three promises.

We can also use Promise.all() to handle errors from any of the input promises. For example, we can write a function that returns a promise that rejects after a given time with a given reason:

function rejectAfter(ms, reason) {
  return new Promise((_, reject) => {
    setTimeout(() => {
      reject(reason);
    }, ms);
  });
}

We can use Promise.all() to catch the reason of the first promise that rejects among three promises:

Promise.all([
  resolveAfter(1000, "A"),
  rejectAfter(2000, "B"),
  rejectAfter(3000, "C")
]).catch(reason => {
  console.log(reason); // B
});

The returned promise will be rejected with “B” after 2000 milliseconds, because that is the first promise to reject.

Wait for Multiple JavaScript Promises to Settle with Promise.allSettled()

Sometimes, we may want to execute multiple asynchronous operations in parallel and get the settlement status of all of them. For example, we may want to fetch data from different sources and handle both successful and failed responses, or we may want to perform some validations on different inputs and collect the results. In these cases, we can use the Promise.allSettled() method to wait for multiple promises to settle, no matter whether they’ll be fulfilled or rejected.

The Promise.allSettled() method accepts an array (or any other iterable) of promises as a parameter. It returns a Promise object that is fulfilled with an array of settlement objects. Each settlement object describes the settlement of the corresponding input promise.

  • If the input promise is fulfilled, the settlement object has a status property of “fulfilled” and a value property of the fulfillment value.

  • If the input promise is rejected, the settlement object has a status property of “rejected” and a reason property of the rejection reason.

Notice that the Promise object returned from the Promise.allSettled()method is only rejected if there’s an error iterating over the input promises. In all other cases, it is fulfilled.

For example, let’s say we have two functions that return promises that resolve or reject after a given time with a given value or reason:

function resolveAfter(ms, value) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(value);
    }, ms);
  });
}

function rejectAfter(ms, reason) {
  return new Promise((_, reject) => {
    setTimeout(() => {
      reject(reason);
    }, ms);
  });
}

We can use Promise.allSettled() to get the settlement objects of four promises that resolve or reject after different times:

Promise.allSettled([
  resolveAfter(1000, "A"),
  rejectAfter(2000, "B"),
  resolveAfter(3000, "C"),
  rejectAfter(4000, "D")
]).then(objects => {
  console.log(objects); // [{status: "fulfilled", value: "A"}, {status: "rejected", reason: "B"}, {status: "fulfilled", value: "C"}, {status: "rejected", reason: "D"}]
});

The returned promise will be fulfilled with an array of objects after 4000 milliseconds, because that is the longest time among the four promises.

We can also use Promise.allSettled() to handle both fulfilled and rejected promises in different ways. For example, we can write a function that takes an array of promises as a parameter and returns a new promise that resolves with an array of successful values and logs the errors for the failed ones:

function handleAll(promises) {
  return Promise.allSettled(promises).then(objects => {
    const values = [];
    for (const object of objects) {
      if (object.status === "fulfilled") {
        values.push(object.value);
      } else {
        console.error(object.reason);
      }
    }
    return values;
  });
}

The handleAll function creates a promise that iterates over the settlement objects and collects the values of the fulfilled ones in an array. It also logs the reasons of the rejected ones to the console. It then returns a promise that resolves with the array of values.

We can use the handleAll function to handle four promises that resolve or reject after different times:

handleAll([
  resolveAfter(1000, "A"),
  rejectAfter(2000, "B"),
  resolveAfter(3000, "C"),
  rejectAfter(4000, "D")
]).then(values => {
  console.log(values); // ["A", "C"]
});

The returned promise will be resolved with an array of values [“A”, “C”] after 4000 milliseconds, and it will also log the errors B and D to the console.

Wait for the Fastest JavaScript Promise to Be Fulfilled with Promise.any()

Sometimes, we may want to execute multiple asynchronous operations in parallel and get the result of the first one that succeeds. For example, we may want to fetch data from different sources and use the first response that is valid, or we may want to try different strategies and use the first one that works. In these cases, we can use the Promise.any() method to race multiple promises against each other and find the first promise to be fulfilled.

The Promise.any()method accepts an array (or any other iterable) of promises as a parameter. It returns a Promise object that is fulfilled with the value of the first input promise to fulfill:

  • As soon as any input promise is fulfilled, the returned Promise object is fulfilled with that value.

  • If all input promises are rejected, the returned Promise object is rejected with an AggregateError which has an errors property containing an array of all rejection reasons.

For example, let’s say we have two functions that return promises that resolve or reject after a given time with a given value or reason:

function resolveAfter(ms, value) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(value);
    }, ms);
  });
}

function rejectAfter(ms, reason) {
  return new Promise((_, reject) => {
    setTimeout(() => {
      reject(reason);
    }, ms);
  });
}

We can use Promise.any() to get the value of the first promise that resolves among four promises:

Promise.any([
  rejectAfter(1000, "A"),
  resolveAfter(2000, "B"),
  rejectAfter(3000, "C"),
  resolveAfter(4000, "D")
]).then(value => {
  console.log(value); // B
});

The returned promise will be fulfilled with “B” after 2000 milliseconds, because that is the first promise to resolve.

We can also use Promise.any() to handle errors from all of the input promises. For example, we can write a function that takes an array of promises as a parameter and returns a new promise that rejects with an array of reasons if all promises fail:

function handleAny(promises) {
  return Promise.any(promises).catch(error => {
    if (error instanceof AggregateError) {
      throw error.errors;
    } else {
      throw error;
    }
  });
}

The handleAny function creates a promise that calls Promise.any() with the input promises and catches the error. If the error is an instance of AggregateError, it throws the errors property which is an array of reasons. Otherwise, it throws the error as it is.

We can use the handleAny function to handle four promises that reject after different times:

handleAny([
  rejectAfter(1000, "A"),
  rejectAfter(2000, "B"),
  rejectAfter(3000, "C"),
  rejectAfter(4000, "D")
]).catch(reasons => {
  console.log(reasons); // ["A", "B", "C", "D"]
});

The returned promise will be rejected with an array of reasons [“A”, “B”, “C”, “D”] after 4000 milliseconds, because all promises are rejected.

Await a JavaScript Promise in an async Function with the await Operator

The await operator is used to wait for a promise to settle. It pauses the execution of an async function until the promise is either fulfilled or rejected. Once the promise settles, the await expression returns the value of the fulfilled promise, or throws the rejected value.

To use the await operator, you must first define an async function. An async function is a function that can contain await expressions.

Once you have defined an async function, you can use the await operator to wait for a promise to settle. To do this, simply pass the promise to the await operator. The await operator will pause the execution of the async function until the promise settles.

Once the promise settles, the await expression will return the value of the fulfilled promise, or throw the rejected value.

For example, the following code shows how to use the await operator to wait for a promise to settle:

async function fetchData(url) {
  const response = await fetch(url);
  const data = await response.json();
  return data;
}

const data = await fetchData("https://example.com/api/data");

console.log(data); // { name: "John Doe", age: 30 }

In this example, the fetchData() function is an async function that takes a URL as a parameter and returns a promise that resolves to the JSON data from the URL. The fetchData() function uses the await operator to wait for the promise to settle before returning the data.

The data variable is assigned the result of calling the fetchData() function. Since fetchData() is an async function, the await operator is used to wait for the promise to settle before assigning the data to the data variable.

Once the data variable is assigned, the console.log() function is used to log the data to the console.

The await operator can be used to wait for any promise to settle, including promises returned from asynchronous functions, promises returned from libraries, and promises returned from promises. This makes the await operator a powerful tool for writing asynchronous code in JavaScript.

Summary:

Promises are a powerful feature of JavaScript that allow us to write asynchronous code in a more efficient and readable way. In this article, we have learned about the following:

  • Promises: What they are, how to create them, and how to handle them.

  • Chaining promises: How to use the then() method to chain together multiple promises and execute them in sequence.

  • Handling errors: How to use the catch() method to handle errors in a promise chain.

  • Executing cleanup logic: How to use the finally() method to execute cleanup logic regardless of whether a promise is fulfilled or rejected.

  • Creating rejected promises: How to use the Promise.reject() function to create a rejected promise.

  • Creating new promises: How to use the Promise constructor to create a new promise.

  • Waiting for the fastest promise: How to use the Promise.race() function to wait for the fastest promise to settle.

  • Waiting for multiple promises to fulfill: How to use the Promise.all() function to wait for multiple promises to be fulfilled.

  • Waiting for multiple promises to settle: How to use the Promise.allSettled() function to wait for multiple promises to settle, regardless of whether they are fulfilled or rejected.

  • Waiting for the fastest promise to be fulfilled: How to use the Promise.any() function to wait for the fastest promise to be fulfilled.

  • Awaiting promises in async functions: How to use the await operator to wait for a promise to settle in an async function.

Let's connect: