JavaScript is single-threaded
- It can execute only one task at a time.
- If a task takes too long (e.g., waiting for network data), the whole program would freeze.
To prevent blocking:
- JS uses asynchronous operations — tasks that can start now but finish later without stopping the main thread.
Examples:
- Fetching data from an API (fetch)
- Reading files (fs.readFile in Node.js)
- Timers (setTimeout, setInterval)
Think of it like ordering food at a restaurant: You order → The chef cooks while you chat → The waiter delivers when ready. You don't stand in the kitchen waiting.
Synchronous (Blocking):
console.log('A');
console.log('B');
console.log('C');Output:
A
B
C
Each line waits for the previous one to finish.
Asynchronous (Non-blocking):
console.log('A');
setTimeout(() => console.log('B'), 1000);
console.log('C');Output:
A
C
B
The timeout lets the program keep going while it waits.
JavaScript runtime (like V8 in Chrome, Node.js) has:
- Call Stack → Runs code line by line
- Web APIs (in browsers) or C++ APIs (in Node.js) → Handle async tasks like timers, DOM events, network requests
- Callback/Task Queues → Stores callbacks ready to run
- Event Loop → Keeps checking if the stack is empty, then pushes queued callbacks into it
📦 Execution flow:
- JS runs code line by line in the Call Stack.
- If it sees an async task (e.g., setTimeout), it hands it off to Web APIs.
- Web APIs finish the task later and push the callback into the Queue.
- The Event Loop checks:
- Is the stack empty? If yes → Take a task from the queue and run it.
- Repeat forever.
There are two main task queues:
Macrotask Queue (Callback Queue):
- Regular async tasks like:
- setTimeout
- setInterval
- DOM events
- Run after all microtasks are done.
Microtask Queue:
- High-priority tasks like:
- Promise.then()
- queueMicrotask()
- MutationObserver
- Always run before the next macrotask.
console.log('Start');
setTimeout(() => console.log('Timeout'), 0);
Promise.resolve().then(() => console.log('Promise'));
console.log('End');Output:
Start
End
Promise
Timeout
Explanation:
- "Start" & "End" run immediately (sync code).
- Promise callback (microtask) runs before Timeout (macrotask).
┌─────────────────┐
│ Call Stack │ ← executes code
└────────┬────────┘
│
┌────────▼────────┐
│ Web APIs │ ← handles timers, network, etc.
└────────┬────────┘
│
┌────────────────▼────────────────┐
│ Task Queue (Macrotasks) │
└────────────────┬────────────────┘
│
┌────────▼────────┐
│ Event Loop │ ← checks stack, moves tasks
└─────────────────┘
Microtasks have their own queue, checked immediately after the current task ends, before any macrotask.
Short version:
"JavaScript is single-threaded, so to avoid blocking, it uses asynchronous operations handled outside the call stack by Web APIs. The Event Loop constantly checks if the stack is empty, then pushes ready callbacks from task queues. Microtasks (like Promises) run before macrotasks (like setTimeout), ensuring critical async work finishes quickly."
Example interview question: ❓ "Why does Promise.then run before setTimeout?" ✅ Because Promise callbacks go into the microtask queue, which is processed before the macrotask queue in each event loop tick.
setTimeout(fn, 0)is not immediate — it waits for the current execution and all microtasks first.- Infinite microtasks can block macrotasks (e.g., recursive
Promise.resolve().then(...)). - In Node.js, the Event Loop has extra phases (Timers, I/O callbacks, Check, Close callbacks).