Event Loop & Call Stack in JavaScript
The call stack is where JavaScript keeps track of what function is currently running. Every time you call a function, it gets added (pushed) to the top of the stack. When that function finishes, it gets removed (popped) from the stack.
JavaScript is single-threaded — it can only do one thing at a time. There is only one call stack, and it processes one function at a time, from top to bottom.
Real-world analogy: Think of the call stack like a stack of plates. You can only add a plate to the top or remove a plate from the top. The last plate you added is the first one removed. This is called LIFO — Last In, First Out.
function greet() {
console.log("Hello!");
}
function sayGoodbye() {
console.log("Goodbye!");
}
function main() {
greet(); // greet() pushed onto stack, runs, then popped
sayGoodbye(); // sayGoodbye() pushed, runs, then popped
}
main(); // main() is pushed first
// Call stack steps:
// 1. main() ← pushed
// 2. greet() ← pushed on top of main()
// 3. greet() ← popped (finished)
// 4. sayGoodbye() ← pushed on top of main()
// 5. sayGoodbye() ← popped (finished)
// 6. main() ← popped (finished)
When functions call other functions, the stack grows. JavaScript cannot move on to the next function until the current one is done. If the stack gets too deep — for example, through an infinite recursive loop — you get the famous Maximum call stack size exceeded error.
function a() {
b();
}
function b() {
c();
}
function c() {
console.log("Deep inside c()");
}
a();
// Stack at the deepest point: a() → b() → c()
// c() runs, then c is popped, then b is popped, then a is popped
JavaScript is single-threaded, but it can still handle things like timers, network requests, and user events without freezing the page. The mechanism that makes this possible is called the event loop.
The event loop constantly monitors the call stack. When the stack is empty, it checks if there are any pending callbacks waiting to run, and moves them into the stack one by one.
Real-world analogy: Think of a restaurant waiter. The waiter takes your order (your code), sends it to the kitchen (a Web API like setTimeout), and then goes off to serve other tables. When your food is ready (the timer finishes), the waiter brings it back to you. The waiter does not stand at the kitchen window waiting — they keep working. That is exactly how JavaScript handles async tasks.
console.log("1 - Start");
setTimeout(function() {
console.log("3 - Inside setTimeout");
}, 0);
console.log("2 - End");
// Output:
// 1 - Start
// 2 - End
// 3 - Inside setTimeout
Even though the delay is 0, the setTimeout callback does not run immediately. It is handed off to the browser's Web API, placed in the callback queue when it is ready, and only runs after all synchronous code has finished. This is the event loop in action.
Web APIs are features provided by the browser — not by JavaScript itself. Things like setTimeout, fetch, addEventListener, and XMLHttpRequest are all Web APIs. They live outside the JavaScript engine.
When you call setTimeout, JavaScript hands the timer off to the browser and immediately moves on. The browser counts the time in the background. JavaScript does not wait — it keeps running the next lines of code. This is what makes JavaScript non-blocking.
Real-world analogy: When you order food at a restaurant, you do not stand at the kitchen window waiting for your meal. You go back to your table, chat with friends, and the waiter brings your food when it is ready. Web APIs are the kitchen — they work in the background so you do not have to wait.
console.log("Before fetch");
// fetch() is a Web API — handed off to the browser
fetch("https://api.example.com/data")
.then(function(response) {
return response.json();
})
.then(function(data) {
console.log("Data received:", data);
});
console.log("After fetch — this runs before data arrives");
// Output order:
// Before fetch
// After fetch — this runs before data arrives
// Data received: { ... } ← arrives later via event loop
Notice that "After fetch" prints before the data arrives. That is because fetch is non-blocking. JavaScript hands the request to the browser and keeps running. When the browser gets the response back, the .then() callbacks are queued up and run when the call stack is empty.
- setTimeout / setInterval — timer functions handled by the browser
- fetch / XMLHttpRequest — network requests handled by the browser
- addEventListener — DOM event handling
- geolocation, localStorage, IndexedDB — browser storage and device APIs
When a Web API finishes its work — for example, when a timer expires — it does not immediately push the callback back onto the call stack. Instead, the callback goes into the callback queue (also called the task queue or macrotask queue).
The event loop keeps watching the call stack. When the stack is completely empty — meaning all synchronous code has finished — the event loop takes the first callback from the queue and pushes it onto the stack to run.
Real-world analogy: Imagine a queue of customers waiting to be served at a checkout counter. The cashier (event loop) can only serve one customer at a time. They serve the next customer only when the current one is fully done. Customers in the queue wait their turn — no cutting in line.
console.log("A");
setTimeout(function() {
console.log("C - from callback queue");
}, 1000);
setTimeout(function() {
console.log("D - also from callback queue");
}, 500);
console.log("B");
// Output:
// A
// B
// D - also from callback queue (500ms fires first)
// C - from callback queue (1000ms fires second)
Both setTimeout callbacks go into the callback queue when their timers expire. The one with the shorter delay (500ms) enters the queue first and runs first. The event loop picks them up in order, one at a time, only when the call stack is empty.
Promises use a special, higher-priority queue called the microtask queue. Microtasks always run before any callbacks from the regular callback queue — even if the callback timer has already expired.
After each task on the call stack finishes, JavaScript drains the entire microtask queue before moving on to the next task in the callback queue. This means all .then() and .catch() handlers from Promises will run before any setTimeout callback.
Real-world analogy: Priority boarding on a flight. Even if regular passengers have been waiting longer in the callback queue, passengers with priority boarding (microtasks) always get to board first. Every. Single. Time.
console.log("1 - Start");
setTimeout(function() {
console.log("4 - setTimeout callback");
}, 0);
Promise.resolve().then(function() {
console.log("3 - Promise .then()");
});
console.log("2 - End");
// Output:
// 1 - Start
// 2 - End
// 3 - Promise .then() ← microtask runs before setTimeout
// 4 - setTimeout callback
Even though setTimeout has a delay of 0, the Promise .then() runs first. This is because Promises go into the microtask queue, which is always processed before the callback queue. This behaviour surprises many developers — understanding it is key to writing correct async JavaScript.
Other things that go into the microtask queue:
- Promise .then(), .catch(), and .finally()
- async/await — it is built on Promises, so await resumes inside the microtask queue
- queueMicrotask() — explicitly schedules a microtask
- MutationObserver callbacks
Now let us put it all together. When JavaScript runs code, it always follows this priority order:
- 1. Synchronous code — runs first, line by line, straight on the call stack
- 2. Microtask queue — Promise callbacks run next, after the current task finishes but before any macrotasks
- 3. Callback queue (macrotask queue) — setTimeout, setInterval, and event callbacks run last
This is exactly what interviewers love to ask about. If you understand this order, you can predict the output of any async JavaScript snippet.
console.log("1 - Sync: Start");
setTimeout(function() {
console.log("5 - Macrotask: setTimeout");
}, 0);
Promise.resolve().then(function() {
console.log("3 - Microtask: Promise 1");
}).then(function() {
console.log("4 - Microtask: Promise 2");
});
console.log("2 - Sync: End");
// Output:
// 1 - Sync: Start
// 2 - Sync: End
// 3 - Microtask: Promise 1
// 4 - Microtask: Promise 2
// 5 - Macrotask: setTimeout
Step by step, here is what happens:
- Step 1: console.log("1 - Sync: Start") runs immediately — it is synchronous
- Step 2: setTimeout is handed off to the browser. Its callback goes into the callback queue when the timer expires
- Step 3: Promise.resolve().then(...) schedules the first .then() in the microtask queue
- Step 4: console.log("2 - Sync: End") runs — still synchronous
- Step 5: The call stack is now empty. JavaScript drains the microtask queue — Promise 1 runs, which chains Promise 2, which also runs
- Step 6: The microtask queue is empty. Now the event loop picks up the setTimeout callback from the callback queue
- Call Stack: JavaScript is single-threaded. The call stack tracks which function is currently running using LIFO (Last In, First Out) order.
- Single-threaded: JavaScript can only do one thing at a time. Long-running synchronous code will block everything else.
- Web APIs: Features like setTimeout and fetch are provided by the browser, not JavaScript itself. They run in the background so JavaScript can keep moving.
- Callback Queue: When a Web API finishes, its callback is placed in the callback queue (macrotask queue). The event loop moves it to the stack only when the stack is empty.
- Microtask Queue: Promises and async/await use the microtask queue. Microtasks always run before callbacks from the callback queue.
- Execution order: Synchronous code → microtasks → callback queue. Master this and you can predict the output of any async snippet.
- Event Loop: The event loop is the coordinator. It watches the call stack and queues, moving tasks into the stack when the stack is free.
- What is the JavaScript call stack and how does it work?
- JavaScript is single-threaded. How does it handle asynchronous operations?
- What is the event loop and what problem does it solve?
- What is the difference between the callback queue (macrotask queue) and the microtask queue?
- Why does a Promise.then() callback run before a setTimeout(fn, 0) callback?
- What are Web APIs? Give three examples.
- What is the output order of this code: console.log(1), setTimeout(fn, 0), Promise.resolve().then(fn), console.log(2)?
- What does it mean for JavaScript to be "non-blocking"?
- What happens if you have a very long synchronous operation (e.g., a loop running millions of times)?
- What is the difference between setImmediate, setTimeout(fn, 0), and Promise.resolve().then(fn) in terms of execution order?
- How does async/await relate to the microtask queue?
- What is a stack overflow and what causes it in JavaScript?