Asynchronous JavaScript & The EVENT LOOP — From Scratch

Note: The Call Stack will execute any execution context that enters it — no exceptions.
TL;DR: The Call Stack has no timer — it just runs what's inside it, line by line.
Understanding JavaScript in the Browser
JavaScript, by design, is single-threaded and synchronous. But wait — if it's synchronous, how does it handle things like network requests, timers, or user interactions without blocking the browser?
Here’s the secret: JavaScript alone can’t do it all. It relies on the browser environment to handle tasks that would otherwise require multithreading or system-level capabilities.
What Powers the JavaScript Runtime?
When you run JavaScript in the browser, it works hand-in-hand with:
The JavaScript Engine (e.g., V8 in Chrome)
The Call Stack
The Web APIs (provided by the browser)
The Callback Queue
The Microtask Queue
And the Event Loop (the orchestrator)
🔌 Web APIs: The Superpowers from the Browser
Web APIs are not part of JavaScript. They’re features provided by the browser — available to JavaScript via the global window object. Examples include:
setTimeout()– scheduling tasksfetch()– making HTTP requestsdocument– interacting with the DOMconsole.log()– printing to the dev consolelocalStorage– storing data in the browser
While we often use these without the window. prefix (like setTimeout()), behind the scenes they are accessed as window.setTimeout().
🛠️ A Simple Asynchronous Code Example
Let’s take a look at a basic async operation:
console.log("Start");
setTimeout(function cb() {
console.log("Timer expired");
}, 5000);
console.log("End");
What Happens Here?
A Global Execution Context (GEC) is created and pushed to the Call Stack.
console.log("Start")is executed → “Start” is printed.setTimeout(cb, 5000)is encountered:The
setTimeoutWeb API is triggered.The callback
cb()is registered, and a 5-second timer starts.
console.log("End")is executed → “End” is printed.After 5 seconds, the timer expires — but the callback
cb()cannot go straight to the Call Stack.Instead, it goes to the Callback Queue.
The Event Loop checks if the Call Stack is empty.
Once the Call Stack is clear, it pushes
cb()into it and executes → “Timer expired” is printed.
🔄 Event Loop & Callback Queue Explained
The Event Loop is the invisible mechanism that ensures JavaScript remains non-blocking.
It constantly monitors the Call Stack and Callback Queue.
If the Call Stack is empty and there’s something in the Callback Queue, it pushes it onto the Call Stack.
This is how asynchronous code is eventually executed.
Why Do We Need a Callback Queue?
Imagine a button clicked six times rapidly:
document.getElementById("btn").addEventListener("click", function cb() {
console.log("Button clicked");
});
Each click triggers the callback cb():
These callbacks are stored in the Callback Queue.
The Event Loop pushes them one by one into the Call Stack when it's free.
This queuing mechanism avoids chaos and maintains the execution order.
🧬 Promises & The Microtask Queue
Here’s where it gets interesting. Let’s consider this:
console.log("Start");
setTimeout(function cbT() {
console.log("Timeout callback");
}, 5000);
fetch("https://api.example.com").then(function cbF() {
console.log("Fetch callback");
});
console.log("End");
Step-by-Step Breakdown:
“Start” is logged.
setTimeout()registerscbT()with a 5-second delay.fetch()initiates a network request and registerscbF()to be executed once the response arrives (say in 2 seconds).“End” is logged.
After 2 seconds,
cbF()enters the Microtask Queue.After 5 seconds,
cbT()enters the Callback Queue.The Event Loop gives priority to the Microtask Queue:
It empties the Microtask Queue first →
cbF()is executed.Then it processes the Callback Queue →
cbT()is executed.
Output:
Start
End
Fetch callback
Timeout callback
⚖️ Microtask Queue vs Callback Queue
| Feature | Microtask Queue | Callback (Task) Queue |
| Priority | High | Lower |
| Examples | Promise.then(), MutationObserver | setTimeout(), setInterval(), click handlers |
| Execution Timing | After current execution, before next task | After the current task completes |
| Risk of Starvation | High (if tasks are recursive) | Low |
Starvation occurs if the Microtask Queue keeps adding new tasks recursively. This can delay or completely block the Callback Queue from being processed.
Common Interview Questions
1. What is the Event Loop in JavaScript?
The Event Loop continuously checks the Call Stack and Callback/Microtask Queues. If the Call Stack is empty, it pushes the next queued task (prioritizing Microtask Queue) onto the Call Stack.
2. What’s the difference between Microtask Queue and Callback Queue?
Microtasks (like Promise.then) are processed before tasks in the Callback Queue (setTimeout, DOM events). Microtasks have higher priority.
3. Is console.log() part of JavaScript?
No. It’s part of the browser’s console Web API, accessed via the window object.
4. What happens if setTimeout is set to 0ms?
Even with 0ms, the callback is queued and must wait for the Call Stack to be clear. So, it's never truly "immediate".
5. Why do event listeners stay in memory after code execution?
Event listeners are registered in the Web API environment. They stay alive to respond to events unless manually removed or the page is unloaded. This is why you should remove listeners when they’re no longer needed to avoid memory leaks.
6. Are synchronous callbacks also handled by the Web API environment?
No. Only asynchronous callbacks (like from setTimeout, fetch, etc.) are registered with Web APIs. Synchronous callbacks used with methods like map, filter, or reduce are handled directly by the JavaScript engine.
✅ Final Thoughts
JavaScript may be single-threaded, but with the help of the browser’s Web APIs, Callback and Microtask Queues, and the mighty Event Loop — it becomes incredibly powerful and responsive.
Mastering this internal flow will transform your debugging skills, optimize your performance logic, and give you a major edge in interviews.
Stay curious. Debug deeply. Learn beyond the syntax. 🚀




