Logo
Published on

How to Resolve JavaScript Promises Sequentially (one by one)

Introduction

JavaScript promises are a powerful tool for handling asynchronous operations. They allow you to write cleaner and more readable code by avoiding callback hell. In this guide, we will learn how to resolve promises sequentially, one by one.

Table of Contents

Let's say you have a set of asynchronous tasks to do.

All three tasks do some operation and then return a promise. What these tasks actually do is not really important just now. We can, for now, just assume that they return a Promise at least.

We know that if we execute them sequentially, there is no guarantee that the completion of their execution will happen in the exact order which in which they are called.

Thus, if we write:

const task1 = async () => {
  /** implementation */
  console.log('task1 done')
}

const task2 = async () => {
  /** implementation */
  console.log('task2 done')
}

const task3 = async () => {
  /** implementation */
  console.log('task3 done')
}

const callTasks = () => {
  task1()
  task2()
  task3()
}

callTasks()

Your JavaScript engine will print these console.log statements in a non-deterministic order.

This is especially true when the task is network bound.

This behavior is not a problem at all when these tasks are independent of one another. What that means is that the result of one task doesn't depend on the result of another.

But often as what happens in real life, your async requests will depend on one another for further progress. This is when restricting execution of these async tasks in a particular order is desirable.

In this post, we will be discussing some ways in which we can control the control flow of async operations in your JavaScript code.

Known Number of Promises

The ES6 way of doing things async way is .then() and .catch().

According to JavaScript Promise spec, every promise is thenable.

BTW, thenable just means a function which exposes a then method. If you have ever dealt with Promises, you will generally deal with .then and .catch methods.

Now, that the context is set, we can come back to our example from above.

Forcing execution in order

const callTasks = () => task1().then(task2).then(task3).catch(console.error)

callTasks()

Here, it it guaranteed that the order of execution will be task1 => task2 => task3.

callTasks():
  task1() ---- done
  ----------------- task2() -------- done
  -------------------------------------- task3() ---------- done

Forcing execution for a few tasks

const callTasks = () => {
  task1().catch(console.error)

  return task2().then(task3).catch(console.error)
}

callTasks()

Here, we are calling the task1() independently of the other two tasks. The execution flow will be as follows:

callTasks():
  task1() ---------------------------------------- done ------------------
  task2() -------------------- done --------------------------------------
  -------------------------------- task3() -------------------------- done

In this case, all three tasks are start and end at different intervals.

And thus, they also end at different intervals of time. But, it’s noteworthy that task3 only start when the task2 is done. This is because we called task3 in a .then clause after task2.

Unknown Number of Promises

The above examples were for the case when you have a known number of promises. For a list of Promises we can do away with a for of, and Array.prototype.reduce methods.

Using the for of loop

const tasks = [task1, task2, task3, ...taskN];

const callTasks = () => {
  for (const task of tasks) {
    await task();
  }
};

callTasks();

Using Array.prototype.reduce

const tasks = [task1, task2, task3, ...taskN]

const callTasks = () => {
  return tasks.reduce((prev, task) => {
    return prev.then(task).catch((err) => {
      console.warn('err', err.message)
    })
  }, Promise.resolve())
}

callTasks()

You might ask why not use the forEach or the map function for handling promises sequentially.

And the answer for that is that they don't work well with async operations.

The forEach loop iterates over the array without considering the return function value of the executor function.

The map function, although cares about the return value of the executor function, ignores the async keyword or any Promisified operations.

So, that leaves our beloved reduce to do the job.

The good thing about reduce is that, unlike map it will wait for the return value of the executor function before the next iteration.

Resolving Promises Recursively

Recursive code is cool to write πŸ™‚

But, I personally avoid it. Iterative code is easier on the eyes and my brain.

Here, we will be using recursion to call the promise executor function repeatedly until all promises have resolved.

const getPromise = async (promise) => {
  return Promise.resolve().then(() => promise)
}

const callTasks = async (promises) => {
  try {
    const promise = await getPromise(promises.shift())

    if (!promises.length) {
      return promise
    }

    return callTasks(promises)
  } catch (err) {
    console.error(err.message)

    return promise
  }
}

callTasks([task1, task2, task3])

Using Generators

Generators are the cool kids in the block. We can defer the next Promise resolution on each function call of callTasks function.

async function* callTasks(promises) {
  for (const promise of promises) {
    yield await promise
  }
}

const promises = [task1, task2, task3]

callTasks(promises) // resolve task1
callTasks(promises) // resolve task2
callTasks(promises) // resolve task3

It's an alternative way to write your code without actually writing looping logic.

This should be enough for you to start with writing Promise based code which resolves sequentially.