Do Promises Block the Event Loop?
Just because a function is asynchronous does not mean that it won't block the main thread (event loop) in a Node application. Learn how to free up the event loop to handle other tasks.
Table of Contents 📖
Promises and the Event Loop
Node offloads asynchronous tasks to the system. However, even though the task below is asynchronous, it will still block the main thread. This means that if we hit the /asynchronous route, any other request will have to wait until the task has completed.
async function asyncTask() {
return new Promise((resolve) => {
for (let i = 0; i < 100; i++) {
for (let j = 0; j < 10000; j++) {
console.log('async', i, j);
}
}
resolve();
});
}
app.get('/asynchronous', async (req, res) => {
await asyncTask();
console.log('Done async task');
return res.sendStatus(200);
});
app.get('/hello', (req, res) => {
res.send('Hello, World!');
});
The reason for this is that the heavy work (quadratic time) is being done synchronously inside the promise executor function. In other words, this will run in a single event loop iteration before resolve() is called. So even though we are using a Promise and async/await, the inner for loops are still blocking operations.
Making it Asynchronous
To make this function truly asynchronous and avoid blocking the main thread, we can break it into smaller chunks using setImmediate(), setTimeout(), or process.nextTick(). These functions allow us to offload some work to the system, freeing up the event loop to work on other tasks between iterations. To demonstrate, lets use the setImmediate() function.
INFO: setImmediate schedules the execution of a callback function to run after the current event loop cycle has completed but before any I/O events or timers (like setTimeout) are processed in the next iteration. Most importantly, it does not block the event loop.
async function asyncTask() {
return new Promise((resolve) => {
let i = 0;
const totalI = 100;
const totalJ = 10000;
function doWork() {
if (i < totalI) {
for (let j = 0; j < totalJ; j++) {
console.log('async', i, j);
}
i++;
// Yield to event loop before continuing to the next iteration
setImmediate(doWork);
} else {
resolve();
}
}
doWork();
});
}