Loading...
Loading...
How JavaScript handles async. Single-threaded but never blocked.
Module 10 · Browser Internals & Best Practices
Production JavaScript requires more than syntax. The event loop, rendering pipeline, bundlers, debugging techniques, performance optimization, design patterns, testing, and security practices separate hobby scripts from professional software.
JavaScript runs on one thread yet handles clicks, timers, and fetch responses without freezing. The event loop dequeues the next task whenever the call stack empties — the core of all async behavior.
Writing JavaScript that works in a tutorial is different from writing JavaScript that ships to production. The event loop explains why setTimeout(0) does not run immediately. Performance profiling finds bottlenecks before users do.
This module covers the internals and best practices that separate hobby scripts from professional software: debugging, bundlers, design patterns, testing strategies, and security fundamentals every developer should know.
The event loop explains setTimeout(fn, 0), why promises beat timeouts in ordering, and why a long loop locks the UI. It is the single most important concept for understanding how async JavaScript works in browsers and Node.
JavaScript runs one thing at a time — always.
// JavaScript has ONE thread of execution. // It can only do one thing at a time. console.log("First"); // runs console.log("Second"); // waits for First to finish console.log("Third"); // waits for Second to finish // So how does async work? // The EVENT LOOP coordinates between: // 1. The Call Stack (where code runs) // 2. Web APIs (where async operations wait) // 3. Task Queues (where callbacks line up) // JS itself is single-threaded, // but the BROWSER is not — it has many threads // for timers, network, DOM, etc.
The actors:
Call Stack — where functions execute (one at a time)
Web APIs — browser handles timers, fetch, DOM events
Task Queue — callbacks waiting for the stack to empty
Event Loop — moves tasks from queue → stack when empty
The stack tracks what's currently executing.
function multiply(a, b) { return a * b; } function square(n) { return multiply(n, n); } function printSquare(n) { const result = square(n); console.log(result); } printSquare(4); // Call Stack progression: // 1. [printSquare] // 2. [printSquare, square] // 3. [printSquare, square, multiply] // 4. [printSquare, square] ← multiply returns // 5. [printSquare] ← square returns // 6. [printSquare, console.log] // 7. [printSquare] ← console.log returns // 8. [] ← printSquare returns
Functions push onto the stack when called, pop off when they return. Only the top frame is executing.
The event loop's job: move queued tasks to the stack.
// The Event Loop algorithm (simplified): // // while (true) { // 1. Run all synchronous code until the stack is empty // 2. Run ALL microtasks (promises, queueMicrotask) // 3. Render update (if needed — ~60fps = every 16ms) // 4. Pick ONE macrotask from the queue (setTimeout, etc.) // 5. Go back to step 1 // } // Visual: // // ┌─────────────────────────────┐ // │ Call Stack │ ← code runs here // └─────────────┬───────────────┘ // │ empty? // ┌─────────────▼───────────────┐ // │ Microtask Queue │ ← drain ALL // └─────────────┬───────────────┘ // │ empty? // ┌─────────────▼───────────────┐ // │ Render (if needed) │ // └─────────────┬───────────────┘ // │ // ┌─────────────▼───────────────┐ // │ Macrotask Queue │ ← pick ONE // └─────────────┬───────────────┘ // │ // loop back ↑
Where async callbacks wait their turn.
console.log("1: start"); setTimeout(() => { console.log("2: timeout"); // → macrotask queue }, 0); console.log("3: end"); // Output: "1: start", "3: end", "2: timeout" // Why? Even with 0ms delay: // 1. console.log("1: start") → runs immediately (stack) // 2. setTimeout callback → goes to Web API, then queue // 3. console.log("3: end") → runs immediately (stack) // 4. Stack is now empty → event loop picks up timeout callback // 5. console.log("2: timeout") → finally runs // The key insight: // setTimeout(fn, 0) doesn't mean "run immediately" // It means "run as soon as the stack is empty" // (minimum delay is actually ~4ms in browsers)
// Multiple tasks queue in order: setTimeout(() => console.log("A"), 0); setTimeout(() => console.log("B"), 0); setTimeout(() => console.log("C"), 0); // Output: A, B, C (FIFO — first in, first out) // Longer delays queue later: setTimeout(() => console.log("slow"), 100); setTimeout(() => console.log("fast"), 10); // Output: fast, slow (10ms fires before 100ms) // But remember: times are MINIMUM delays // If the stack is busy, the callback waits longer: setTimeout(() => console.log("delayed"), 10); // If synchronous code takes 500ms, // this callback runs after ~500ms, not 10ms
Predict the order of async operations.
console.log("1"); setTimeout(() => console.log("2"), 0); Promise.resolve().then(() => console.log("3")); console.log("4"); // Output: 1, 4, 3, 2 // // Why this order: // "1" — synchronous, runs immediately // setTimeout → callback goes to MACROTASK queue // Promise.then → callback goes to MICROTASK queue // "4" — synchronous, runs immediately // Stack empty → drain microtasks first → "3" // Then pick one macrotask → "2" // Rule: Microtasks ALWAYS run before macrotasks // (when both are waiting)
// More complex example: console.log("A"); setTimeout(() => { console.log("B"); Promise.resolve().then(() => console.log("C")); }, 0); Promise.resolve().then(() => { console.log("D"); setTimeout(() => console.log("E"), 0); }); console.log("F"); // Output: A, F, D, B, C, E // A — sync // F — sync // D — microtask (runs before any macrotask) // B — first macrotask (setTimeout from top) // C — microtask created inside B's macrotask // E — second macrotask (setTimeout from D's microtask)
Common questions about event loop.