Promise: How to Use and Some Notes!
Tags: javascript, node, promise
Published: 2024-06-09 07:24:44

It would be a glaring omission not to talk about Promise in JavaScript. In fact, there have been many articles written about Promise, which you can find through Google or occasionally come across in a programming-related community. But because Promise is an important concept and everyone has a different way of explaining it, I still decided to write this article.  

When I first learned JavaScript, Promise was the most confusing thing. I thought I understood it and knew how to use it, but in reality, there were still many long and painful stumbles that taught me valuable lessons. I read many articles about Promise in both English and Vietnamese, and gradually everything started to make sense, helping me understand and use it correctly.  

This article will not go into the concepts of Promise but rather touch on some notes and possible misunderstandings. It aims to provide readers with a general understanding and help them avoid some mistakes when writing code.  

## What is a Promise?

A Promise is an object that represents a future result. In other words, a Promise represents the result of an asynchronous function.  

A Promise has three states corresponding to the three possible outcomes of an asynchronous function:

- `pending` is the initial state, waiting for the result.  
- `fulfilled` is the successful state, with a result value.  
- `rejected` is the failure state, with an optional error value.  

![Promise](https://static-img.2coffee.dev/promise-cach-su-dung-va-mot-vai-luu-y_promise-flow.webp)

For example, let's create a Promise that takes a number `x` and returns the `fulfilled` state if `x` is divisible by 2, and the `rejected` state otherwise.  

```js
function isEven(x) {
  return new Promise((resolve, reject) => {
    if (x % 2 === 0) {
      resolve(true);
    } else {
      reject(new Error('x is not even'));
    }
  });
}
```

In essence, a Promise represents a future result. In the above example, creating a Promise is not necessary because all the actions inside the `isEven` function are synchronous. So, when should we use a Promise and what makes a function asynchronous?  

What makes a function asynchronous? I have mentioned it in many articles. Some I/O tasks provided by asynchronous functions cannot immediately return a result; they depend on external factors such as hardware speed, network speed, etc. Waiting for these actions can waste a lot of time or cause serious bottlenecks.  

For example, when making a GET request to the address `https://example.com`, the processing does not simply depend on CPU speed anymore; it also depends on your network speed. The faster the network, the quicker you receive the result. In JavaScript, we have the `fetch` function to send the request, and it is asynchronous, returning a Promise.  

```js
fetch('https://example.com');
```

To handle the result of a Promise, we use `then` and `catch` for success and failure cases, respectively.  

```js
fetch('https://example.com')
    .then(res => console.log(res))
    .catch(err => console.log(err));
```

If after some processing time, `fetch` is `fulfilled`, the function inside `then` will be activated. Otherwise, if `fetch` is `rejected`, the function inside `catch` will be executed immediately.  

Sometimes you may come across questions like predicting the result of the following example:  

```js
console.log('1');

fetch('https://example.com')
    .then(res => console.log(res))
    .catch(err => console.log(err))
    .finally(() => console.log('3'));

console.log('2');
```

This question is actually to test your understanding of asynchronous behavior in JavaScript. The result will be 1, 2, 3 instead of 1, 3, 2. This is because of the nature of asynchronous processing. Since the result of asynchronous behavior will be returned in the future, JavaScript thinks, "OK, this asynchronous function does not have an immediate result yet. Let it be and continue processing the following commands. When everything is done, check if it has a result." For more information on how asynchronous processing works, you can refer to articles on [Asynchronous Programming in JavaScript](https://2coffee.dev/bai-viet/lap-trinh-bat-dong-bo-la-gi-tai-sao-javascript-la-ngon-ngu-lap-trinh-bat-dong-bo).  

Promise has some useful static methods for various use cases, such as `all`, `allSettled`, `any`, and `race`.  

`Promise.all` takes an array of Promises, returns a Promise, and is in the `fulfilled` state when all Promises in the array are successful. Otherwise, it is in the `rejected` state when at least one Promise in the array fails.  

`Promise.allSettled` is similar to `Promise.all` but always returns the results of all Promises in the array regardless of success or failure. Both `Promise.all` and `Promise.allSettled` are useful when you need to run multiple asynchronous functions immediately without caring about the order of the results.  

On the other hand, `Promise.race` returns the result of the Promise that is settled first, regardless of success or failure. `Promise.any` returns the first fulfilled Promise in the array. `race` and `any` are suitable when you have multiple Promises that perform similar actions and need a fallback between the results.  

## Promise Replaces "Callback Hell"

It is surprising to know that JavaScript used to not have Promise. Yes, you heard it right. This fact led Node.js to also not have Promise in its early days, and that is the biggest regret the creator of Node.js has. 

Previously, all asynchronous tasks were handled through callbacks. We would define a callback function to handle the result in the future.  

For example, an early asynchronous request function was `XMLHttpRequest`. It handled the result through a callback.  

```js
function reqListener() {
  console.log(this.responseText);
}

const req = new XMLHttpRequest();
req.addEventListener('load', reqListener);
req.open('GET', 'https://example.com');
req.send();
```

`reqListener` is a function that is called when there is a result from the request to `https://example.com`. Handling asynchronous behavior with callbacks brought some troubles, including what is commonly known as "callback hell".  

For example, an asynchronous function `fnA` takes a callback function with two parameters: `data` representing the successful result and `err` representing the error. Similar functions include `fnB`, `fnC`, etc. If we want to combine these processing functions together, we need to nest them inside each other.  

```js
fnA((data1, err1) => {
    fnB((data2, err2) => {
        fnC((data3, err3) => {
        ....  
        });
    });
});
```

When there are too many callbacks written like this, our code becomes a literal "callback hell". It becomes messy and hinders the reading and understanding process.  

Promise was introduced to bring a new approach to handling asynchronous behavior. We still use callbacks, but we are able to limit the "hell" by connecting everything sequentially through `then`.  

```js
fnA()
    .then(data => fnB(data))
    .then(data => fnC(data))
    ...  
    .catch(err => console.log(err));
```

After that, many asynchronous callback-based functions were rewritten using `new Promise`. In Node.js, we even have a built-in module called [util.promisify](https://nodejs.org/api/util.html#utilpromisifyoriginal) specifically for this transformation. Callbacks are still supported, but with the benefits that Promise brings, many new libraries are using Promise as the default for handling asynchronous behavior.  

## Promise in Loops

There are many unfortunate mistakes when using Promise without fully understanding its essence and one of them is handling sequential asynchronous operations in a loop.  

Suppose you need to iterate through 5 pages, making paginated API calls from 1 to 5 and concatenate the return values in order into an array.  

```js
const results = [];
for (let i = 1; i <= 5; i++) {
  fetch(`https://example.com?page=${i}`)
    .then(response => response.json())
    .then(data => results.push(data));
}
```

The above code creates a loop to fetch data from page 1 to page 5, and the returned data is pushed into the `results` array. At first glance, the result would be an array of data in the order from the first page to the last page. However, in reality, each time it runs, you will find that the data in `results` is randomly ordered.  

Remember `fetch`, it returns a Promise, representing a future result... While `for` is trying to loop through as quickly as possible, you can think of it as all 5 `fetch` commands being called and started "almost instantly". At this point, CPU speed is less important - it's the network speed that determines which `fetch` command has the first result. As soon as it has the result, it immediately pushes it into `results`, resulting in the data being added randomly.  

To solve this issue, there are several ways. For example:  

```js
const results = [];
fetch('https://example.com?page=1')
  .then(response => response.json())
  .then(data => {
    results.push(data);
    return fetch('https://example.com?page=2');
  })
  .then(response => response.json())
  .then(data => {
    results.push(data);
    return fetch('https://example.com?page=3');
  })
  ...  
```

This is crazy, and nobody writes code like this. Imagine what if there were 1000 pages? It's a joke, but the example above attempts to illustrate the idea of how to wait for the previous request to complete before proceeding to the next request.  

[Bluebird](https://www.npmjs.com/package/bluebird) is a very good Promise library that provides many utility functions to make working with asynchronous functions easier.  

The above example can be rewritten using the `each` function provided by Bluebird.  

```js
const results = [];
Promise.each([1, 2, 3, 4, 5], (i) => {
  return fetch(`https://example.com?page=${i}`)
    .then(response => response.json())
    .then(data => results.push(data));
});
```

## Async/Await - The Missing Piece of Promise?

To create a Promise, we use the syntax `new Promise`, but with async/await, we simply declare an `async` function.  

```js
async function isEven(x) {
  if (x % 2 === 0) {
    return true;
  } else {
    throw new Error('x is not even');
  }
}
```

While Promise uses `then` to handle the return result at some point in the future, async/await is as simple as using `await`.  

```js
const result = await fetch('https://example.com');
const resultJSON = await result.json();
```

Starting from Node.js version 14.8, we have the top-level await feature, which means `await` calls are no longer limited to inside an `async` function; they can be used outside as well. Before that, `await` could only be used inside an `async` function.  

```js
async function getData() {
    const result = await fetch('https://example.com');
    const resultJSON = await result.json();
}
```

While Promise uses `.catch` to handle errors, async/await uses `try...catch`.  

```js
async function getData() {
    try {
        const result = await fetch('https://example.com');
        const resultJSON = await result.json();
    } catch (err) {
        console.log(err);
    }
}
```

It is evident that async/await allows us to write asynchronous code as if it were synchronous, without callbacks and without the need for `then`. We simply wait for the result using `await`.  

However, this can also lead to some dangerous misunderstandings, such as forgetting the `await` keyword to wait for the result of an asynchronous behavior or mistakenly thinking that an asynchronous function is a synchronous one.  

```js
async function getData() {
    try {
        const result = await fetch('https://example.com');
        const resultJSON = await result.json();
        return resultJSON;
    } catch (err) {
        console.log(err);
    }
}

function main() {
    const data = getData();
    console.log(data);
}
```

In essence, `getData` returns a Promise, so the above program does not work correctly if the intention is to get data from an API call. To fix this, we need to change it.  

```js
async function main() {
    const data = await getData();
    console.log(data);
}
```

## return vs. return await

Sometimes you may come across code that looks like this:  

```js
async function fn() {
    ...  
    return await asyncFn();
}
```

The function `fn` is returning an `await` of the asynchronous function `asyncFn`. The author probably intended for `fn` to always return the result of `asyncFn`, as `await` is waiting for the result of the asynchronous function, effectively turning `fn` into a synchronous function, or in other words, "not" returning a Promise.  

Unfortunately, this is a dangerous misunderstanding. In essence, `await` is only meant to be used at the top-level or inside an `async` function, and if it is `async`, it must return a Promise. Therefore, `fn` always returns a Promise. So why use `return await asyncFn()`?  

Simply using `return asyncFn()` can save you a bit of keystrokes, as there is no significant difference compared to `return await asyncFn()`. The big change happens when there is a `try...catch` in `return`.  

Let's consider the following two functions:  

```js
async function rejectionWithReturnAwait () {
  try {
    return await Promise.reject(new Error())
  } catch (e) {
    return 'Saved!'
  }
}

async function rejectionWithReturn () {
  try {
    return Promise.reject(new Error())
  } catch (e) {
    return 'Saved!'
  }
}
```

The first function, `rejectionWithReturnAwait`, is intentionally using `return await`, and when this Promise is rejected, the `catch` block quickly catches the error and executes the `return 'Saved!'` statement. This means the function returns a Promise containing the string 'Saved'.  

In contrast, `rejectionWithReturn`, without `await`, is attempting to reject a `Promise.reject(new Error())`, and the `catch` block is never executed.  

References:

- [Promise - Mozilla](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)
- [Top-level await - Node.js v14.8.0](https://nodejs.org/en/blog/release/v14.8.0)