JavaScript is single-threaded, meaning it can only execute one task at a time on the main thread. There is only one call stack. But JavaScript can still handle asynchronous tasks like fetching data, timers, and UI events using the event loop, Web APIs, and callback queues.
Understanding how these components work together is essential for writing non-blocking, responsive applications.
The Key Players
There are five major components you need to understand:
1. Call Stack
2. Heap
3. Web APIs
4. Callback / Task Queue
5. Event Loop
Call Stack (Execution Context Stack)
The call stack is managed by the JavaScript Engine (like V8). It’s where functions get pushed (called) and popped (returned). Only one function executes at a time.
function greet() {
console.log("Hello");
}
greet();
Execution steps:
push greet() → execute → pop greet()
If the call stack is full (too many nested calls), you get a Stack Overflow error.
Heap
The heap is managed by the JavaScript Engine for memory allocation. Objects, arrays, and closures are stored here dynamically. It is used by the garbage collector for memory management.
Web APIs
Web APIs are browser-provided features, not part of the JavaScript language itself. They run outside the JavaScript engine, allowing asynchronous operations.
When you call something like setTimeout(), fetch(), addEventListener(), XMLHttpRequest(), DOM events, or Geolocation API, these functions are handled by the browser’s Web API environment, not the JavaScript engine.
Example
console.log("Start");
setTimeout(() => {
console.log("Inside Timeout");
}, 2000);
console.log("End");
Step-by-Step Process
- console.log(“Start”) executes immediately (stack)
- setTimeout() is passed to Web API, timer runs in browser (not blocking JS)
- console.log(“End”) executes immediately
- After 2 seconds, Web API pushes callback to the Callback Queue
- Event Loop checks if the call stack is empty, then moves the callback from the queue and executes it
Output:
Start
End
Inside Timeout
Callback / Task Queue (Macrotask Queue)
The callback queue stores callbacks from asynchronous operations like timers, events, and network responses. It waits until the call stack is empty before pushing a callback back for execution. This is handled by the Event Loop.
Examples of macrotasks:
- setTimeout
- setInterval
- I/O callbacks
- DOM events
Event Loop
The Event Loop is the manager that constantly checks: “Is the call stack empty?”
If yes, it moves tasks (callbacks, promises, etc.) from their queues into the call stack for execution.
The Loop Process (Simplified)
while (true) {
if (callStack.isEmpty()) {
eventLoop.moveNextTaskFromQueueToStack();
}
}
It’s like a traffic controller between the JavaScript engine (call stack), Web APIs, and queues (task and microtask).
Microtasks vs Macrotasks
JavaScript has two kinds of queues that the event loop manages differently.
Microtask Queue
- Handles promises, MutationObservers, and queueMicrotask()
- Higher priority than macrotasks
- Executed immediately after the current call stack is empty, before any macrotasks
Promise.resolve().then(() => console.log("Microtask"));
queueMicrotask(() => console.log("Also Microtask"));
Macrotask Queue
- Handles setTimeout, setInterval, fetch callbacks, DOM events, etc.
- Executed after microtasks are done
Execution Order Example
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
console.log("4");
Step-by-Step:
- console.log(“1”) executes
- setTimeout() goes to Web API, then callback to macrotask queue
- Promise.then() goes to microtask queue
- console.log(“4”) executes
- Call stack empty, Event loop checks microtask queue first and runs “3”
- Then macrotask queue runs “2”
Output:
1
4
3
2
Full Flow Diagram
┌────────────────────────────┐
│ JavaScript Engine │
│ ┌──────────────┐ │
│ │ Call Stack │◄──────────┐│
│ └──────────────┘ ││
└──────────┬─────────────────┘│
│ │
▼ │
┌──────────────────┐ │
│ Web APIs │ │
└──────────────────┘ │
│ │
▼ │
┌──────────────────────┐ │
│ Callback Queue │◄──────┘
└──────────────────────┘
│
▼
┌────────────────┐
│ Event Loop │
└────────────────┘
│
▼
┌────────────────┐
│ Call Stack │
└────────────────┘
Example: fetch() and Event Loop
console.log("Start");
fetch("https://api.example.com")
.then(() => console.log("Fetch done"));
console.log("End");
Execution:
- “Start” logged immediately
- fetch() is handled by Web API for the network request asynchronously
- “End” logged immediately
- After fetch completes, .then() callback goes to microtask queue
- Event Loop executes the .then() callback next
Output:
Start
End
Fetch done
Summary of All Components
| Component | Managed By | Handles | Priority |
|---|---|---|---|
| Call Stack | JS Engine | Running current function | — |
| Heap | JS Engine | Memory (objects, closures) | — |
| Web APIs | Browser | Async tasks (timers, DOM, fetch) | — |
| Microtask Queue | Event Loop | Promises, MutationObserver | High |
| Macrotask Queue | Event Loop | setTimeout, I/O, events | Low |
| Event Loop | Browser | Coordinates everything | — |
Key Takeaways
The JavaScript engine executes one task at a time. The Web APIs handle async work like timers and network requests in the background. The Event Loop constantly checks the call stack. Once the stack is empty, it moves microtasks (like promises) first, then macrotasks (like timeouts) into execution.
This makes JavaScript non-blocking and asynchronous, even though it’s single-threaded.
Analogy
Think of:
- Call Stack: The Chef (can cook one dish at a time)
- Web APIs: Kitchen assistants (boil water, bake, fetch data)
- Callback Queue: Completed orders waiting to be served
- Event Loop: The Waiter, checking if the chef is free to serve next order
- Microtasks: VIP orders (promises) that always get served first
Understanding the event loop and how JavaScript coordinates between synchronous execution and asynchronous operations is fundamental to building responsive web applications.