JavaScript is single-threaded, meaning it can only execute one piece of code at a time. Yet it handles asynchronous operations like network requests, timers, and user events without freezing the browser or server. How is this possible? The answer lies in JavaScript's event-driven, non-blocking I/O model powered by the Event Loop. Understanding this mechanism is crucial for writing efficient async code and debugging timing issues.
Code executes line by line, one at a time. Each operation must complete before the next begins.
console.log("Step 1");
console.log("Step 2");
console.log("Step 3");
// Output:
// Step 1
// Step 2
// Step 3Problem with synchronous code:
console.log("Fetching data...");
// This blocks everything for 5 seconds!
const start = Date.now();
while (Date.now() - start < 5000) {
// Blocking the thread
}
console.log("Done!"); // Only prints after 5 secondsOperations can be initiated and resumed later without blocking the main thread.
console.log("Step 1");
setTimeout(() => {
console.log("Step 2 (after 2 seconds)");
}, 2000);
console.log("Step 3");
// Output:
// Step 1
// Step 3
// Step 2 (after 2 seconds)Step 2 doesn't block Steps 1 and 3. The callback is scheduled to run later.
JavaScript was designed for browsers. Multiple threads manipulating the same DOM simultaneously would create race conditions and make web development incredibly complex. Instead, JavaScript uses a single thread with an event loop to handle concurrency elegantly.
Note: While JavaScript itself is single-threaded, the runtime environment (browser or Node.js) uses multiple threads behind the scenes for I/O operations, timers, and other tasks.
To understand async JavaScript, you need to know about these components:
A data structure that tracks function execution. Last In, First Out (LIFO).
function first() {
second();
console.log("First done");
}
function second() {
third();
console.log("Second done");
}
function third() {
console.log("Third done");
}
first();Call Stack Flow:
1. [main]
2. [first] [main]
3. [second] [first] [main]
4. [third] [second] [first] [main]
5. [second] [first] [main] ← third() completes
6. [first] [main] ← second() completes
7. [main] ← first() completes
Browser-provided APIs that operate outside the JavaScript engine:
setTimeout,setIntervalfetch/XMLHttpRequest- DOM events (
addEventListener) console.log
These APIs handle operations in separate threads and callback into JavaScript when done.
A queue of callback functions waiting to be executed. FIFO (First In, First Out).
A higher-priority queue for Promises and queueMicrotask callbacks.
The orchestrator that continuously checks:
- Is the Call Stack empty?
- If yes, move the oldest task from the Microtask Queue to the Call Stack
- If Microtask Queue is empty, move the oldest task from the Callback Queue to the Call Stack
- Repeat
console.log("Script start");
setTimeout(() => {
console.log("setTimeout");
}, 0);
Promise.resolve().then(() => {
console.log("Promise 1");
});
Promise.resolve().then(() => {
console.log("Promise 2");
});
console.log("Script end");Execution Flow:
| Step | Call Stack | Web APIs | Callback Queue | Microtask Queue | Console Output |
|---|---|---|---|---|---|
| 1 | console.log("Script start") |
- | - | - | "Script start" |
| 2 | setTimeout(cb, 0) |
Timer set | - | - | - |
| 3 | Promise.resolve().then(cb1) |
- | - | Promise 1 | - |
| 4 | Promise.resolve().then(cb2) |
- | - | Promise 1, Promise 2 | - |
| 5 | console.log("Script end") |
- | - | Promise 1, Promise 2 | "Script end" |
| 6 | - | Timer done → cb | setTimeout | Promise 1, Promise 2 | - |
| 7 | Promise 1 callback | - | setTimeout | Promise 2 | "Promise 1" |
| 8 | Promise 2 callback | - | setTimeout | - | "Promise 2" |
| 9 | setTimeout callback | - | - | - | "setTimeout" |
Final Output:
Script start
Script end
Promise 1
Promise 2
setTimeout
Key Rule: Microtasks (Promises) always execute before macrotasks (
setTimeout,setInterval, I/O callbacks).
┌─────────────────────────────────────────┐
│ JavaScript Engine │
│ ┌─────────────┐ ┌───────────────┐ │
│ │ Call Stack │ │ Heap (Memory)│ │
│ │ │ │ │ │
│ │ function() │ │ Objects │ │
│ │ function() │ │ Closures │ │
│ └─────────────┘ └───────────────┘ │
└─────────────────────────────────────────┘
↑
│ checks if empty
┌─────┴─────┐
│ Event Loop│
└─────┬─────┘
│
┌───────────────┼───────────────┐
↓ ↓ ↓
┌──────────┐ ┌──────────┐ ┌──────────────┐
│Microtask │ │ Callback │ │ Web APIs │
│ Queue │ │ Queue │ │ │
│Promises │ │setTimeout│ │setTimeout │
│queueMicro│ │DOM events│ │fetch │
│task │ │I/O │ │DOM events │
└──────────┘ └──────────┘ └──────────────┘
setTimeoutsetIntervalsetImmediate(Node.js)- I/O operations
- UI rendering
requestAnimationFrame
Promise.then/catch/finallyqueueMicrotask()MutationObserver
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
Promise.resolve().then(() => {
console.log("4");
Promise.resolve().then(() => console.log("5"));
});
setTimeout(() => console.log("6"), 0);
console.log("7");
// Output:
// 1
// 7
// 3
// 4
// 5
// 2
// 6Execution Order:
- All synchronous code
- All microtasks (and microtasks created during microtask execution)
- One macrotask
- Back to step 2
console.log("A");
setTimeout(() => console.log("B"), 0);
console.log("C");
// Output:
// A
// C
// BEven with a delay of 0ms, the callback goes to the Callback Queue and waits for:
- The Call Stack to clear
- All microtasks to complete
Note: The actual minimum delay is typically 4ms in browsers (HTML5 spec), even if you specify 0.
console.log("Start");
// This blocks everything!
const start = Date.now();
while (Date.now() - start < 3000) {
// CPU-intensive work
}
console.log("End"); // 3 seconds later
// Any pending async callbacks are delayed by 3 seconds
setTimeout(() => console.log("Timeout"), 0);1. Break into chunks:
function processLargeArray(arr) {
const chunkSize = 1000;
let index = 0;
function processChunk() {
const chunk = arr.slice(index, index + chunkSize);
// Process chunk...
index += chunkSize;
if (index < arr.length) {
setTimeout(processChunk, 0); // Yield to event loop
}
}
processChunk();
}2. Web Workers (Browser):
const worker = new Worker("worker.js");
worker.postMessage({ data: largeArray });
worker.onmessage = (event) => {
console.log("Result:", event.data);
};3. Worker Threads (Node.js):
const { Worker } = require("worker_threads");
const worker = new Worker("./worker.js");// Run after current stack clears
setTimeout(() => {
console.log("Deferred");
}, 0);
// Or using Promises (microtask, runs sooner)
Promise.resolve().then(() => {
console.log("Microtask deferred");
});async function processItems(items) {
for (const item of items) {
await processItem(item);
// Allows browser to render between iterations
await new Promise(resolve => setTimeout(resolve, 0));
}
}let data;
fetch("/api/data")
.then(res => res.json())
.then(json => {
data = json;
});
console.log(data); // undefined! Fetch hasn't completed yet.function loop() {
Promise.resolve().then(loop);
}
loop();
// This starves the macrotask queue! setTimeout callbacks never run.setTimeout(() => console.log("1"), 100);
setTimeout(() => console.log("2"), 50);
setTimeout(() => console.log("3"), 0);
// Output: 3, 2, 1 (not 1, 2, 3!)let count = 0;
function increment() {
const current = count;
setTimeout(() => {
count = current + 1;
}, 0);
}
increment();
increment();
// count might not be 2 due to the read-modify-write race!console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => {
console.log("3");
setTimeout(() => console.log("4"), 0);
});
Promise.resolve().then(() => console.log("5"));
console.log("6");What is the exact output order?
Trace this code through the Call Stack, Web APIs, and queues:
console.log("Start");
setTimeout(() => {
console.log("Timeout 1");
Promise.resolve().then(() => console.log("Promise inside timeout"));
}, 0);
setTimeout(() => console.log("Timeout 2"), 0);
Promise.resolve().then(() => console.log("Promise 1"));
Promise.resolve().then(() => console.log("Promise 2"));
console.log("End");Write a function that pauses for a given time without blocking the event loop.
async function delay(ms) {
// Your code
}
async function main() {
console.log("Start");
await delay(1000);
console.log("After 1 second");
}Implement a scheduler that ensures tasks don't block the main thread for more than a given time slice.
class TaskScheduler {
// Implement runTask(task, timeSlice)
// Breaks task into chunks if it exceeds timeSlice
}Explain why this outputs in this specific order and how changing the delay affects it:
setTimeout(() => console.log("timeout"), 0);
Promise.resolve().then(() => console.log("promise"));- JavaScript is single-threaded but handles async operations via the Event Loop
- The Call Stack tracks synchronous execution (LIFO)
- Web APIs handle async operations outside the main thread
- The Callback Queue holds macrotasks (
setTimeout, I/O) - The Microtask Queue holds Promise callbacks (higher priority)
- The Event Loop moves tasks from queues to the Call Stack when it's empty
- Microtasks always run before macrotasks
setTimeout(fn, 0)doesn't run immediately — it goes to the Callback Queue- Never block the Event Loop with long-running synchronous operations
Now that you understand the async foundation:
- Callbacks — the original async pattern
- Promises — cleaner async handling
- Async/Await — synchronous-looking async code
Happy coding! 🚀