AiTechWorlds
AiTechWorlds
JavaScript is single-threaded — it can only execute one thing at a time. Yet it handles timers, network requests, and user events without freezing the page. The event loop is how this works, and understanding it eliminates an entire class of confusing bugs.
JavaScript keeps track of function calls with a stack (last in, first out):
function greet(name) {
return `Hello, ${name}!`;
}
function printGreeting() {
const msg = greet("Alice");
console.log(msg);
}
printGreeting();
// Call stack at each step:
// 1. [printGreeting]
// 2. [greet, printGreeting] ← greet pushed on top
// 3. [printGreeting] ← greet returned, popped
// 4. [] ← printGreeting returned
When the stack is empty, JavaScript is idle — ready to pick up the next task from the queue.
Browsers (and Node.js) provide asynchronous Web APIs that run outside the call stack:
console.log("1 - Start");
setTimeout(() => {
console.log("3 - Timeout");
}, 0);
console.log("2 - End");
// Output: 1, 2, 3
// Even setTimeout(fn, 0) doesn't run immediately!
// The callback goes to the task queue and waits for the call stack to empty
The event loop constantly checks: "Is the call stack empty? If so, move the next task from the queue to the stack."
Not all async operations are equal. There are two queues:
Microtask queue (higher priority):
.then() / .catch() / .finally() callbacksqueueMicrotask()MutationObserverMacrotask queue (lower priority):
setTimeout / setInterval callbacksRule: Microtasks run to completion before any macrotask runs.
console.log("1");
setTimeout(() => console.log("2 - setTimeout"), 0);
Promise.resolve().then(() => console.log("3 - Promise"));
console.log("4");
// Output: 1, 4, 3, 2
// 1 and 4: synchronous, run immediately
// 3: microtask, runs before setTimeout
// 2: macrotask, runs last
console.log("Start");
setTimeout(() => console.log("Timeout 1"), 0);
Promise.resolve()
.then(() => {
console.log("Promise 1");
return Promise.resolve("inner");
})
.then(v => console.log("Promise 2:", v));
setTimeout(() => console.log("Timeout 2"), 0);
console.log("End");
// Output:
// Start
// End
// Promise 1 ← microtask
// Promise 2 ← chained microtask runs before macrotasks
// Timeout 1 ← macrotask
// Timeout 2 ← macrotask
// This freezes the browser — no UI updates, no events
function blockingOperation() {
const end = Date.now() + 5000;
while (Date.now() < end) { /* spin */ }
console.log("Done — but UI was frozen for 5 seconds!");
}
// The correct approach: break long work into chunks
function nonBlockingOperation(items) {
let i = 0;
function processChunk() {
const end = Date.now() + 5; // process for 5ms max
while (i < items.length && Date.now() < end) {
process(items[i++]);
}
if (i < items.length) {
setTimeout(processChunk, 0); // yield to event loop
} else {
console.log("Done!");
}
}
processChunk();
}
// Modern approach: Web Workers for truly CPU-heavy work
const worker = new Worker("heavy-task.js");
worker.postMessage(data);
worker.onmessage = (e) => console.log("Result:", e.data);
// One-time delay
const id = setTimeout(callback, 1000);
clearTimeout(id); // cancel before it fires
// Repeating
const intervalId = setInterval(() => {
updateClock();
}, 1000);
// Stop after some condition
const interval = setInterval(() => {
progress += 10;
if (progress >= 100) {
clearInterval(interval);
onComplete();
}
}, 100);
// More reliable recursion pattern (no drift)
function tick() {
doWork();
setTimeout(tick, 1000); // schedule next after this one completes
}
tick();
For smooth animations — runs before the browser paints:
function animate() {
// Update your animation state
element.style.left = (x += 2) + "px";
// Schedule next frame (≈60fps, or display refresh rate)
if (x < 500) {
requestAnimationFrame(animate);
}
}
requestAnimationFrame(animate);
// Cancel
const frameId = requestAnimationFrame(animate);
cancelAnimationFrame(frameId);
Schedule a function in the microtask queue without wrapping in a Promise:
console.log("1");
queueMicrotask(() => console.log("3 - microtask"));
console.log("2");
// Output: 1, 2, 3
// These execute in this order:
// 1. Synchronous code
// 2. Microtasks (promises, queueMicrotask)
// 3. Rendering updates
// 4. Macrotasks (setTimeout, events)
// This matters when:
async function fetchAndRender() {
const data = await fetch("/api/data").then(r => r.json());
// This code runs in the microtask queue after the await resolves
// The DOM won't update between these two lines
updateData(data);
renderUI(data);
}
// setTimeout(fn, 0) is useful for "run after current rendering"
element.classList.add("animating");
setTimeout(() => element.classList.add("running"), 0);
// Forces the browser to paint the first class before adding the second
Next lesson: Callbacks & Callback Hell — the original async pattern and why Promises replaced it.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises