Part 2: Event Loop: The heart of Javascript
Understand how Javascript handles your code during runtime

Introduction
The JavaScript event loop is one of the most fundamental concepts that every developer working with the language should understand. It is at the heart of how JavaScript handles asynchronous operations and ensures that your applications remain responsive. Despite its importance, it’s a concept that often confuses newcomers and experienced developers alike. In this article, we’ll demystify the event loop and explore how it works with detailed explanations and examples.
The Single-Threaded Nature of JavaScript
JavaScript is single-threaded, meaning it can execute only one task at a time in the main thread. This limitation might seem restrictive at first glance, but JavaScript employs the event loop to handle multiple tasks efficiently without blocking the main thread.
Why Single-Threaded?
The single-threaded nature simplifies JavaScript’s design, making it easier to avoid complex issues like race conditions and deadlocks common in multithreaded environments. Instead, JavaScript relies on asynchronous patterns and the event loop to manage concurrency.
The Key Players in the Event Loop
To understand the event loop, we need to introduce its key components:
Call Stack: The call stack is a data structure that keeps track of the function calls in your program. When a function is called, it’s added to the stack. When the function completes, it’s removed from the stack.
Example:
function first() { console.log('First function'); } function second() { first(); console.log('Second function'); } second();Output:
First function Second functionHere,
secondis added to the call stack, thenfirstis added, and finally, both are removed in order.Web APIs: These are provided by the browser or Node.js runtime. Functions like
setTimeout,fetch, or DOM event listeners are part of the Web APIs. They handle operations outside the JavaScript engine and notify the event loop when tasks are ready.Macrotask Queue (or Callback Queue): This queue holds tasks that are ready to be executed after the current function on the call stack finishes. Examples include callbacks from
setTimeoutorsetInterval.Microtask Queue: Microtasks include promises and
MutationObservercallbacks. They have higher priority than tasks in the task queue and are processed before moving on to the next task in the task queue.Event Loop: The event loop is the mechanism that coordinates the execution of code, collecting and processing events, and executing queued tasks and microtasks.
How the Event Loop Works
Let’s break down the process step by step:
The JavaScript engine starts executing code from the top of the file.
When it encounters an asynchronous operation (e.g.,
setTimeout), it delegates the task to the Web APIs and continues executing the rest of the code.Once the asynchronous operation completes, its callback is added to the macrotask queue (or the microtask queue in the case of promises).
The event loop checks if the call stack is empty. If it is, it dequeues tasks from the microtask queue and executes them.
After the microtask queue is empty, the event loop moves to the macrotask queue and processes tasks there.
Example: Understanding Execution Order
Here’s a simple example to illustrate the event loop in action:
console.log('Start');
setTimeout(() => {
console.log('Timeout callback');
}, 0);
Promise.resolve().then(() => {
console.log('Promise callback');
});
console.log('End');
Output:
Start
End
Promise callback
Timeout callback
Why this order?
console.log('Start')andconsole.log('End')execute immediately because they are synchronous.setTimeoutis an asynchronous operation. Its callback is added to the task queue.The promise resolves immediately, and its
.thencallback is added to the microtask queue.After the synchronous code finishes, the event loop processes the microtask queue first, executing the promise callback.
Finally, the event loop processes the task queue and executes the
setTimeoutcallback.
Adding Complexity
Let’s add more asynchronous operations to see how they interact:
console.log('Script start');
setTimeout(() => {
console.log('setTimeout 1');
}, 0);
setTimeout(() => {
console.log('setTimeout 2');
}, 0);
Promise.resolve().then(() => {
console.log('Promise 1');
}).then(() => {
console.log('Promise 2');
});
console.log('Script end');
Output:
Script start
Script end
Promise 1
Promise 2
setTimeout 1
setTimeout 2
Detailed Breakdown:
console.log('Script start')executes immediately.Two
setTimeoutcalls are made, and their callbacks are queued in the task queue.A promise resolves, and its
.thencallback is added to the microtask queue.console.log('Script end')executes.The microtask queue processes
Promise 1andPromise 2in order.The task queue processes
setTimeout 1andsetTimeout 2in order.
Advanced Event Loop Patterns
1. Handling Heavy Computations
// Example: Chunking Heavy Computations
function processLargeArray(array, chunkSize = 1000) {
let index = 0;
function processChunk() {
const chunk = array.slice(index, index + chunkSize);
if (chunk.length === 0) return Promise.resolve();
// Process chunk
chunk.forEach(item => {
// Heavy computation per item
});
index += chunkSize;
// Schedule next chunk using microtask
return new Promise(resolve => {
queueMicrotask(() => {
processChunk().then(resolve);
});
});
}
return processChunk();
}
// Usage
const largeArray = new Array(1000000).fill(0);
processLargeArray(largeArray).then(() => {
console.log('Processing complete');
});
2. Custom Task Scheduling
class TaskScheduler {
constructor() {
this.highPriorityTasks = [];
this.lowPriorityTasks = [];
this.isProcessing = false;
}
addHighPriorityTask(task) {
this.highPriorityTasks.push(task);
this.processNextTask();
}
addLowPriorityTask(task) {
this.lowPriorityTasks.push(task);
this.processNextTask();
}
async processNextTask() {
if (this.isProcessing) return;
this.isProcessing = true;
while (this.highPriorityTasks.length || this.lowPriorityTasks.length) {
const task = this.highPriorityTasks.length
? this.highPriorityTasks.shift()
: this.lowPriorityTasks.shift();
try {
await task();
} catch (error) {
console.error('Task failed:', error);
}
// Allow other tasks to interrupt
await new Promise(resolve => setTimeout(resolve, 0));
}
this.isProcessing = false;
}
}
// Usage
const scheduler = new TaskScheduler();
scheduler.addHighPriorityTask(async () => {
console.log('High priority task');
});
scheduler.addLowPriorityTask(async () => {
console.log('Low priority task');
});
3. Advanced Error Handling
// Example: Robust Error Handling in Async Operations
async function robustAsyncOperation(operation) {
const MAX_RETRIES = 3;
let attempts = 0;
while (attempts < MAX_RETRIES) {
try {
const result = await operation();
return result;
} catch (error) {
attempts++;
if (attempts === MAX_RETRIES) {
throw new Error(`Operation failed after ${MAX_RETRIES} attempts: ${error.message}`);
}
// Exponential backoff
await new Promise(resolve =>
setTimeout(resolve, Math.pow(2, attempts) * 1000)
);
}
}
}
// Usage
async function fetchWithRetry(url) {
return robustAsyncOperation(async () => {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
return response.json();
});
}
Common Pitfalls and Best Practices
Misunderstanding Asynchronous Execution
A common misconception is that setTimeout with a delay of 0 executes immediately. However, its callback is added to the task queue and will only execute after the current stack and microtasks are cleared.
Overloading the Microtask Queue
Microtasks can pile up if not handled properly, leading to performance issues. For example:
function repeat() {
Promise.resolve().then(repeat);
}
repeat();
This code creates an infinite loop of microtasks, effectively freezing the event loop.
Best Practices
Avoid blocking the call stack with heavy computations; use Web Workers if necessary.
Minimize the use of nested
setTimeoutor promises to keep the microtask and task queues manageable.Leverage tools like
async/awaitfor cleaner asynchronous code.
Best Practices for Event Loop Optimization
- Batch DOM Updates
// Bad practice
function updateList(items) {
items.forEach(item => {
const element = document.createElement('li');
element.textContent = item;
document.querySelector('ul').appendChild(element);
});
}
// Better practice
function updateList(items) {
const fragment = document.createDocumentFragment();
items.forEach(item => {
const element = document.createElement('li');
element.textContent = item;
fragment.appendChild(element);
});
document.querySelector('ul').appendChild(fragment);
}
- Use RequestAnimationFrame for Animations
function smoothAnimation() {
let start = null;
const duration = 1000; // 1 second
function animate(timestamp) {
if (!start) start = timestamp;
const progress = timestamp - start;
// Calculate animation state
const percentage = Math.min(progress / duration, 1);
// Apply animation
element.style.transform = `translateX(${percentage * 100}px)`;
if (progress < duration) {
requestAnimationFrame(animate);
}
}
requestAnimationFrame(animate);
}
- Optimize Promise Chains
// Bad practice - sequential
async function fetchAllData(urls) {
const results = [];
for (const url of urls) {
const result = await fetch(url);
results.push(await result.json());
}
return results;
}
// Better practice - parallel with controlled concurrency
async function fetchAllData(urls, concurrency = 3) {
const results = [];
const chunks = urls.reduce((acc, url, i) => {
const chunkIndex = Math.floor(i / concurrency);
acc[chunkIndex] = [...(acc[chunkIndex] || []), url];
return acc;
}, []);
for (const chunk of chunks) {
const chunkResults = await Promise.all(
chunk.map(url =>
fetch(url).then(res => res.json())
)
);
results.push(...chunkResults);
}
return results;
}
Conclusion
The event loop is a powerful mechanism that allows JavaScript to handle asynchronous operations seamlessly. By understanding its workings, you can write more efficient and responsive applications. Remember the hierarchy: synchronous code → microtasks → tasks, and you’ll be well on your way to mastering the event loop.
Experiment with these concepts and code examples to solidify your understanding. Happy coding!




