How to use promises in JavaScript

Quick Overview:
  • Promises are the foundation of asynchronous programming in modern JavaScript.
  • A promise is an object returned by an asynchronous function, which represents the current state of the operation.
    • It provides method to handle success operation.
    • It also provides method to handle failure operation.
  • With a promise-based API, the asynchronous function starts the operation and returns a Promise object.
Example:
  • We will use the fetch() API, which is the modern, promise-based replacement for XMLHttpRequest.
<script type='text/javascript'>	    
const fetchPromise = fetch("https://www.brightenminds.com/2023/08/motivational-monday-quotes.html");
console.log(fetchPromise);
fetchPromise.then((response) => {
  console.log(`Received response: ${response.status}`);
});
console.log("Started request…");
</script>
Explanation:
  • calling the fetch() API, and assigning the return value to the fetchPromise variable
  • immediately after, logging the fetchPromise variable. This should output something like: Promise { <state>: "pending" }, telling us that we have a Promise object, and it has a state whose value is "pending". The "pending" state means that the fetch operation is still going on.
  • passing a handler function into the Promise's then() method. When (and if) the fetch operation succeeds, the promise will call our handler, passing in a Response object, which contains the server's response.
  • logging a message that we have started the request.
Output:
Promise { <state>: "pending" }
Started request…
Received response: 200
Note that Started request… is logged before we receive the response. Unlike a synchronous function, fetch() returns while the request is still going on, enabling our program to stay responsive. The response shows the 200 (OK) status code, meaning that our request succeeded.

Chaining promises

With the fetch() API, once we get a Response object, you need to fetch another resource. In this case, we will use the fetch() API again. So it turns out to be an asynchronous call. So this is a case where we have to call two successive asynchronous functions.
<script type='text/javascript'>
const fetchPromise = fetch("https://www.brightenminds.com/2023/08/motivational-monday-quotes.html");
console.log(fetchPromise);
fetchPromise.then((response) => {
  const fetchPromise2 = fetch("https://www.brightenminds.com/2023/08/how-to-use-asyncawait-with-foreach-loop.html");
  console.log(fetchPromise2);
  fetchPromise2.then((response2) => {
    console.log(`Received response2: ${response2.status}`);
  });
  console.log(`Received response: ${response.status}`);
});
console.log("Started request…");
</script>
Promise { <state>: "pending" }
Started request…
Promise { <state>: "pending" }
Received response: 200
Received response2: 200
In this example, as before, we add a then() handler to the promise returned by fetch(). And again added a then() handler to the promise returned by fetch() action called inside first fetch promise handler.

Promises provides the elegant feature in order to avoid "callback hell". This elegant feature is that then() itself returns a promise, which will be completed with the result of the function passed to it.
Let's rewrite the above code like this:

<script type='text/javascript'>
const fetchPromise = fetch("https://www.brightenminds.com/2023/08/motivational-monday-quotes.html");
fetchPromise.then((response) => fetch("https://www.brightenminds.com/2023/08/how-to-use-asyncawait-with-foreach-loop.html"))
  .then((response2) => {
  console.log(`Received response2: ${response2.status}`);
});
console.log("Started request…");
</script>
Instead of calling the second then() inside the handler for the first then(), we can return the promise returned by second first(), and call the second then() on that return value. This is called promise chaining and means we can avoid ever-increasing levels of indentation when we need to make consecutive asynchronous function calls.
Before we move on to the next step, there's one more piece to add. We need to check that the server accepted and was able to handle the request, before we try to read it. We'll do this by checking the status code in the response and throwing an error if it wasn't "OK":
<script type='text/javascript'>
const fetchPromise = fetch("https://www.brightenminds.com/2023/08/motivational-monday-quotes.html");
fetchPromise.then((response) => {
  if (!response.ok) {
    throw new Error(`HTTP error: ${response.status}`);
  }
  console.log(`Received response: ${response.status}`);
  return fetch("https://www.brightenminds.com/2023/08/how-to-use-asyncawait-with-foreach-loop.html")
})
  .then((response2) => {
  console.log(`Received response2: ${response2.status}`);
});
console.log("Started request…");
</script>

Catching errors

The fetch() API can throw an error for many reasons (for example, because there was no network connectivity or the URL was malformed in some way) and we are throwing an error ourselves if the server returned an error.

To support error handling, Promise objects provide a catch() method. This is a lot like then(): you call it and pass in a handler function. However, while the handler passed to then() is called when the asynchronous operation succeeds, the handler passed to catch() is called when the asynchronous operation fails.

If you add catch() to the end of a promise chain, then it will be called when any of the asynchronous function calls fails. So you can implement an operation as several consecutive asynchronous function calls, and have a single place to handle all errors.

Example below: In second fetch() call, we will call invaid url
<script type='text/javascript'>
const fetchPromise = fetch("https://www.brightenminds.com/2023/08/motivational-monday-quotes.html");
fetchPromise.then((response) => {
	if (!response.ok) {
		throw new Error(`HTTP error: ${response.status}`);
	}
	console.log(`Received response: ${response.status}`);
	return fetch("https://www.brightenminds.com/2023/08/how-to-use-with-foreach-loop.html")
})
.then((response2) => {
	if (!response2.ok) {
		throw new Error(`HTTP error: ${response2.status}`);
	}
	console.log(`Received response2: ${response2.status}`);
})
.catch((error) => {
	console.error(`Could not get products: ${error}`);
});
console.log("Started request…");	
</script>

Promise terminology

Promises come with some quite specific terminology that it's worth getting clear about.
First, a promise can be in one of three states:
  • pending: the promise has been created, and the asynchronous function it's associated with has not succeeded or failed yet. This is the state your promise is in when it's returned from a call to fetch(), and the request is still being made. 
  • fulfilled: the asynchronous function has succeeded. When a promise is fulfilled, its then() handler is called. 
  • rejected: the asynchronous function has failed. When a promise is rejected, its catch() handler is called.
Note that what "succeeded" or "failed" means here is up to the API in question: for example, fetch() considers a request successful if the server returned an error like 404 Not Found, but not if a network error prevented the request being sent. Sometimes, we use the term settled to cover both fulfilled and rejected. A promise is resolved if it is settled, or if it has been "locked in" to follow the state of another promise.

Combining multiple promises

Sometimes, you need all the promises to be fulfilled, but they don't depend on each other. In a case like that, it's much more efficient to start them all off together, then be notified when they have all fulfilled.
The Promise.all() method will solve this problem. It takes an array of promises and returns a single promise.
The promise returned by Promise.all() is:
  • fulfilled when and if all the promises in the array are fulfilled. In this case, the then() handler is called with an array of all the responses, in the same order that the promises were passed into all().
  • rejected when and if any of the promises in the array are rejected. In this case, the catch() handler is called with the error thrown by the promise that rejected.
Example 6:
<script type='text/javascript'> 
const fetchPromise1 = fetch("https://www.brightenminds.com/2023/08/motivational-monday-quotes.html");
const fetchPromise2 = fetch("https://www.brightenminds.com/p/motivational-quotes.html");

Promise.all([fetchPromise1, fetchPromise2])
  .then((responses) => {
  for (const response of responses) {
    console.log(`${response.url}: ${response.status}`);
  }
})
  .catch((error) => {
  console.error(`Failed to fetch: ${error}`);
});
console.log("Started request…");
</script>
How to use promises in JavaScript
Sometimes, you might need any one of a set of promises to be fulfilled, and don't care which one. In that case, you want Promise.any(). This is like Promise.all(), except that it is fulfilled as soon as any of the array of promises is fulfilled, or rejected if all of them are rejected.

Code Repo