
Let’s say you have a set of asynchronous tasks to do.
task1()
task2()
task3()
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 loop
ing logic.
This should be enough for you to start with writing Promise
based code which resolves sequentially.
Leave a Reply