Loading...
Loading...
Two queues, different priorities. Why Promises always beat setTimeout.
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.
Learn Microtasks & Macrotasks in JavaScript — covering Two Queues, Microtasks, Macrotasks, Priority Rules with interactive visual examples and step-by-step explanations.
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.
Understanding microtasks & macrotasks is essential for every JavaScript developer. It shows up in frontend UI code, backend APIs, and framework internals — and getting it wrong leads to bugs that are hard to trace. This chapter builds intuition with visuals so the behavior sticks.
Not all async callbacks are equal.
Microtask Queue
HIGH PRIORITY — drain ALL
Macrotask Queue
LOWER PRIORITY — one at a time
// Quick proof — microtasks beat macrotasks: setTimeout(() => console.log("macro"), 0); Promise.resolve().then(() => console.log("micro")); console.log("sync"); // Output: sync, micro, macro // Even though both have 0 delay, // microtask runs first because it has priority
High-priority tasks that drain completely before anything else.
// Promise callbacks are microtasks: Promise.resolve() .then(() => console.log("micro 1")) .then(() => console.log("micro 2")) .then(() => console.log("micro 3")); // async/await — code after await is a microtask: async function example() { console.log("before await"); // sync await fetch("/api"); // pauses here console.log("after await"); // microtask! } // queueMicrotask — explicitly schedule a microtask: queueMicrotask(() => { console.log("explicit microtask"); }); // MutationObserver — DOM mutation callbacks: const observer = new MutationObserver(() => { console.log("DOM changed"); // microtask });
// Key rule: ALL microtasks drain before moving on // Even microtasks created during microtask processing! Promise.resolve().then(() => { console.log("micro 1"); // This creates a NEW microtask — it runs BEFORE any macrotask: Promise.resolve().then(() => console.log("micro 2")); }); setTimeout(() => console.log("macro"), 0); // Output: micro 1, micro 2, macro // micro 2 was created inside micro 1, but still runs // before the macrotask — the queue is fully drained
Regular tasks — one per event loop iteration.
// setTimeout/setInterval — classic macrotasks: setTimeout(() => console.log("task 1"), 0); setTimeout(() => console.log("task 2"), 0); setTimeout(() => console.log("task 3"), 0); // Each runs in a separate event loop iteration: // Iteration 1: task 1 → drain microtasks → render // Iteration 2: task 2 → drain microtasks → render // Iteration 3: task 3 → drain microtasks → render // DOM events are also macrotasks: button.addEventListener("click", () => { console.log("click handler"); // macrotask Promise.resolve().then(() => console.log("micro in click")); }); // When clicked: "click handler", "micro in click" // The microtask runs before the next macrotask/render // I/O callbacks (fetch completion, file read): fetch("/api").then(r => r.json()).then(data => { // The .then is a microtask, but the fetch completion // that triggers it originates as a macrotask });
The complete execution order.
// The event loop cycle: // 1. Execute synchronous code (call stack) // 2. Drain ALL microtasks // 3. ONE macrotask // 4. Drain ALL microtasks (again) // 5. Render (if needed) // 6. Repeat from step 3 console.log("1: sync"); setTimeout(() => { console.log("4: macro 1"); Promise.resolve().then(() => console.log("5: micro inside macro")); }, 0); setTimeout(() => { console.log("6: macro 2"); }, 0); Promise.resolve().then(() => console.log("2: micro 1")); Promise.resolve().then(() => console.log("3: micro 2")); // Output: 1, 2, 3, 4, 5, 6
Sync runs first, then ALL microtasks, then ONE macrotask, then ALL microtasks created during that macrotask, then next macrotask.
Microtasks can block everything else.
// ⚠️ DANGER: Microtasks that create more microtasks // can starve macrotasks and rendering: function infiniteMicrotasks() { Promise.resolve().then(() => { console.log("micro"); infiniteMicrotasks(); // creates another microtask }); } infiniteMicrotasks(); // The microtask queue NEVER empties! // setTimeout callbacks never run // The page never renders // Same effect as an infinite while loop // Safe pattern — break work into macrotasks: function processItems(items) { if (items.length === 0) return; const item = items.shift(); process(item); setTimeout(() => processItems(items), 0); // yields to render } // Or use requestIdleCallback for low-priority work: requestIdleCallback((deadline) => { while (deadline.timeRemaining() > 0 && tasks.length) { tasks.shift()(); } });
Rule of thumb:
Microtasks for quick continuations (promise chains). Macrotasks (setTimeout) when you need to yield to the browser for rendering or user input.
Common questions about microtasks & macrotasks.