- 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, its 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.