DEV Community

Cover image for From Callback Hell to Promise Chains
Yurich
Yurich

Posted on • Updated on

From Callback Hell to Promise Chains

When programming in JavaScript, we often have to make requests to the server. And as we know, the JS machine does not wait for the request to be done and continues to execute the next code. This approach in JS is called asynchrony.
All requests to the server are promises. Although they do not return any result at the same time, they promise that they will return something, or they will throw an error.

Image description

I want to consider an example of when we need to make several requests to the server or call several promises. Let's create an array of promises.

const promises = [
   Promise.resolve(1),
   ...
   Promise.resolve(n)
]
Enter fullscreen mode Exit fullscreen mode

I just want to call all these promises from a regular loop

for (const promise of promises) {
  promise.then(r => console.log(r))
}
Enter fullscreen mode Exit fullscreen mode

And it will work if we don't have to wait for the result. But most likely we need the data that returns the promises. A special case may be when each subsequent promise must depend on the data of the previous one.
The first thing that comes to mind is to call each subsequent promise in the .then method of the previous one.

promises[0].then(result => {
  console.log(result)
  promises[1].then(result => {
    console.log(result)
    promises[2].then(result => {
      console.log(result)

     ...
    })
  })
})
Enter fullscreen mode Exit fullscreen mode

If you continue to nest promises one inside the other, then such code will be very difficult to read. And only indents help to orientate a little. This approach creates Callback Hell.

Image description

In this case, it would be better if the promise returns another promise. So this code can be reformatted in the promise chain.

promises[0]
  .then(result => {
    console.log(result)
    return promises[1]
  })
  .then(result => {
    console.log(result)
    return promises[2]
  })
  ...
  .then(result => {
    console.log(result)
  })
Enter fullscreen mode Exit fullscreen mode

This approach is much more readable and resembles the Chain of Responsibilities design pattern.

But when working with arrays, it is still easier and clearer to use loops. Let's do it. To do this, we need to create a separate function and use the async/await syntax.

const run = async () => {
  for (const promise of promises) {
    const result = await promise
    console.log(result)
  }
}

run()
Enter fullscreen mode Exit fullscreen mode

This is much shorter and clearer. This approach will work in most cases and will be understandable to a developer of any level.

The fact is that all promises are executed synchronously one after the other and it takes more time than if all requests were executed simultaneously. And in the case when the next promise does not depend on the previous one, and we want to work with the results of all requests, it is better to use special methods all, allSettled, race, any

Let's redesign this functionality so that it works more efficiently.

Promise.all(promises).then((responses) => {
  for (const response of responses) {
    console.log(response);
  }
})
.catch(error => console.error(error))
Enter fullscreen mode Exit fullscreen mode

This is much better, and all promises inside Promise.all are executed asynchronously.
We have considered several approaches to working with the array of promises and their main pros and cons.
I hope this article helps you better understand promises and write better code.

Top comments (0)