Day 26: Event Loop & Concurrency
Day 26: Event Loop & Concurrency
1. Learning Objectives
By the end of this lesson, you will be able to:
- Explain JavaScript's single-threaded, non-blocking execution model
- Describe the components of the event loop: call stack, heap, callback queue, microtask queue
- Distinguish between macrotasks (setTimeout, setInterval, I/O) and microtasks (Promise callbacks, queueMicrotask, MutationObserver)
- Predict the execution order of mixed synchronous, microtask, and macrotask code
- Understand how async/await and Promises interact with the event loop
- Recognize and avoid common pitfalls like blocking the event loop and starving the microtask queue
- Use
requestAnimationFramefor smooth visual updates - Apply concurrency patterns like cooperative yielding and task splitting
2. Theory
JavaScript is Single-Threaded
JavaScript has a single thread with one call stack. It can do only one thing at a time. However, the browser (or Node.js) provides Web APIs (like setTimeout, fetch, DOM events) that run outside the main thread. The event loop coordinates when their callbacks are executed.
The Call Stack
The call stack is a LIFO (Last In, First Out) data structure that tracks function calls. When a function is called, a frame is pushed onto the stack. When it returns, the frame is popped.
function multiply(a, b) { return a * b; }
function square(n) { return multiply(n, n); }
function printSquare(n) { console.log(square(n)); }
printSquare(5); // Stack: printSquare → square → multiply → (returns) → square → printSquare
If the stack grows too large (e.g., infinite recursion), we get a Stack Overflow error.
The Event Loop Components
┌───────────────────────────────────────────┐
│ Call Stack │
│ (synchronous execution) │
└────────────────┬──────────────────────────┘
│
▼
┌───────────────────────────────────────────┐
│ Microtask Queue │
│ (Promise callbacks, queueMicrotask, │
│ MutationObserver) │
│ Processed until empty before next │
│ macrotask │
└────────────────┬──────────────────────────┘
│
▼
┌───────────────────────────────────────────┐
│ Macrotask Queue │
│ (setTimeout, setInterval, I/O, │
│ UI events, setImmediate) │
│ One task per event loop tick │
└───────────────────────────────────────────┘
│
▼
Render (if needed)
requestAnimationFrame callbacks
Event loop algorithm (simplified):
- Execute all synchronous code on the call stack.
- Process all microtasks in the microtask queue (until empty).
- Take one macrotask from the macrotask queue and execute it.
- If rendering is needed, run
requestAnimationFramecallbacks and repaint. - Repeat from step 2.
Macrotasks vs Microtasks
| Type | Examples | Priority |
|---|---|---|
| Synchronous | Regular function calls, console.log, arithmetic | Executes immediately on the call stack |
| Microtasks | Promise.then/catch/finally, queueMicrotask, MutationObserver, process.nextTick (Node.js) | Higher priority — processed before the next macrotask |
| Macrotasks | setTimeout, setInterval, setImmediate (Node.js), I/O callbacks, UI events, postMessage | Lower priority — one per event loop tick |
console.log("1"); // Sync
setTimeout(() => console.log("2"), 0); // Macrotask
Promise.resolve().then(() => console.log("3")); // Microtask
console.log("4"); // Sync
// Output: 1, 4, 3, 2
Explanation:
1and4are synchronous — they run immediately.- The Promise
.then()is a microtask — it runs after all sync code but before the next macrotask. setTimeout(even with 0ms) is a macrotask — it runs after microtasks are drained.
Starving the Microtask Queue
If microtasks continuously add more microtasks, the macrotask queue never gets processed. This can freeze the UI.
function recursiveMicrotask() {
queueMicrotask(() => {
console.log("microtask");
recursiveMicrotask(); // Adds another microtask — infinite loop!
});
}
recursiveMicrotask();
// This will never stop, and setTimeout callbacks never run.
Solution: Use a macrotask (e.g., setTimeout) to break the chain, allowing other tasks to execute.
requestAnimationFrame
requestAnimationFrame schedules a callback before the next repaint. It's ideal for animations and visual updates.
function animate(timestamp) {
element.style.left = `${(timestamp / 10) % 400}px`;
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
requestAnimationFrame callbacks run after microtasks but before the browser repaints. They are not strictly microtasks or macrotasks — they run in the render phase.
Blocking the Event Loop
If a synchronous operation takes too long (e.g., a heavy loop, complex computation, or JSON.parse on a huge object), the entire page freezes because no other code can run.
// BAD: blocks the event loop for 3 seconds
const start = Date.now();
while (Date.now() - start < 3000) {
// busy wait
}
console.log("Done");
// GOOD: yield to the event loop using async/await or setTimeout
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function doWork() {
for (let i = 0; i < 10; i++) {
await sleep(300); // yield control
console.log(`Step ${i}`);
}
}
3. Code Examples
Example 1: Basic Event Loop Order
console.log("=== Event Loop Order ===\n");
console.log("1: Sync start");
setTimeout(() => console.log("2: setTimeout (macrotask)"), 0);
Promise.resolve().then(() => console.log("3: Promise.then (microtask)"));
queueMicrotask(() => console.log("4: queueMicrotask (microtask)"));
setTimeout(() => console.log("5: setTimeout 2 (macrotask)"), 0);
Promise.resolve().then(() => {
console.log("6: Promise.then inner (microtask)");
queueMicrotask(() => console.log("7: microtask inside microtask"));
});
console.log("8: Sync end");
// Output:
// 1: Sync start
// 8: Sync end
// 3: Promise.then (microtask)
// 4: queueMicrotask (microtask)
// 6: Promise.then inner (microtask)
// 7: microtask inside microtask
// 2: setTimeout (macrotask)
// 5: setTimeout 2 (macrotask)
Example 2: Microtask Queue Depth
console.log("=== Microtask Depth ===\n");
console.log("Start");
Promise.resolve()
.then(() => {
console.log("Microtask 1");
return Promise.resolve();
})
.then(() => {
console.log("Microtask 2");
return Promise.resolve();
})
.then(() => {
console.log("Microtask 3");
});
console.log("End");
// Output:
// Start
// End
// Microtask 1
// Microtask 2
// Microtask 3
// All microtasks run before any macrotask, even if they chain.
Example 3: setTimeout(0) vs Promise vs process.nextTick (Node.js)
// Run in Node.js
console.log("Start");
setTimeout(() => console.log("setTimeout"), 0);
setImmediate(() => console.log("setImmediate"));
process.nextTick(() => console.log("nextTick"));
Promise.resolve().then(() => console.log("Promise"));
console.log("End");
// Output:
// Start
// End
// nextTick
// Promise
// setTimeout
// setImmediate
Order in Node.js:
- Sync code
process.nextTick(highest priority microtask)- Other microtasks (Promise callbacks)
- Macrotasks:
setTimeoutthensetImmediate(order may vary depending on phase)
Example 4: Blocking the Event Loop and How to Fix It
// ---- Blocking the Event Loop ----
console.log("=== Blocking Demo ===\n");
function blockFor(ms) {
const start = Date.now();
while (Date.now() - start < ms) {
// Busy wait — blocks everything
}
}
console.log("Before block");
blockFor(2000); // Blocks for 2 seconds — UI freezes, no events processed
console.log("After block");
// ---- Non-blocking Approach ----
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function chunkedWork() {
console.log("Starting chunked work...");
for (let i = 0; i < 5; i++) {
// Simulate a small unit of work
const result = i * i;
console.log(`Chunk ${i}: ${result}`);
// Yield to the event loop — allows other tasks to run
await sleep(0); // Schedule next chunk as a macrotask
}
console.log("Chunked work complete.");
}
chunkedWork();
console.log("This runs immediately — not blocked!");
Example 5: requestAnimationFrame and Rendering
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Animation Frame Demo</title>
<style>
#box {
width: 50px;
height: 50px;
background: red;
position: relative;
}
#log { font-family: monospace; font-size: 12px; margin-top: 20px; }
</style>
</head>
<body>
<div id="box"></div>
<div id="log"></div>
<script>
const box = document.getElementById("box");
const log = document.getElementById("log");
let startTime = null;
let frameCount = 0;
function step(timestamp) {
if (!startTime) startTime = timestamp;
const elapsed = timestamp - startTime;
// Move the box in a circle
const x = 200 + 150 * Math.cos(elapsed / 500);
const y = 100 + 150 * Math.sin(elapsed / 500);
box.style.left = `${x}px`;
box.style.top = `${y}px`;
frameCount++;
if (elapsed > 3000) {
log.textContent = `Animation ran for ${(elapsed / 1000).toFixed(1)}s, ${frameCount} frames`;
return; // stop after 3 seconds
}
requestAnimationFrame(step);
}
requestAnimationFrame(step);
// Demonstrate that requestAnimationFrame runs before repaint
let microtaskCount = 0;
function addMicrotask() {
queueMicrotask(() => {
microtaskCount++;
if (microtaskCount < 5) addMicrotask();
});
}
addMicrotask();
console.log("Microtasks queued — they run before rAF and repaint.");
</script>
</body>
</html>
Example 6: Cooperative Yielding for Heavy Computations
// ---- Splitting a Heavy Task ----
// Simulate a heavy computation (e.g., processing 10 million items)
function heavyTask(items) {
let result = 0;
for (let i = 0; i < items; i++) {
result += Math.sqrt(i);
}
return result;
}
// BAD: blocks the event loop
console.time("Heavy sync");
console.log(heavyTask(10000000));
console.timeEnd("Heavy sync");
// UI freezes for a noticeable time
// GOOD: chunk the work and yield
async function heavyTaskAsync(items, chunkSize = 100000) {
let result = 0;
let i = 0;
function processChunk() {
const end = Math.min(i + chunkSize, items);
for (; i < end; i++) {
result += Math.sqrt(i);
}
if (i < items) {
// Yield to the event loop before the next chunk
return new Promise(resolve => {
setTimeout(() => {
resolve(processChunk());
}, 0);
});
}
return result;
}
return processChunk();
}
async function runHeavyAsync() {
console.time("Heavy async");
const result = await heavyTaskAsync(10000000);
console.log(result);
console.timeEnd("Heavy async");
console.log("UI remained responsive during computation!");
}
runHeavyAsync();
4. Exercises
Beginner
Predict and verify the output of:
console.log("A"); setTimeout(() => console.log("B"), 0); console.log("C");Write code that logs "1" synchronously, then uses a microtask to log "2", and a macrotask to log "3".
What is the output of:
Promise.resolve().then(() => console.log("then")); console.log("sync");
Intermediate
Write a function
measureFrameRate()that usesrequestAnimationFrameto count how many frames occur in 2 seconds.Create a function
yieldingMap(arr, fn, chunkSize)that processes an array in chunks, yielding to the event loop between chunks, and returns a Promise that resolves with the mapped array.Fix this code that potentially starves the microtask queue:
function processItems(items) { items.forEach((item, index) => { queueMicrotask(() => { console.log(index, item); processItems(items.slice(1)); // recursive! }); }); }
Advanced
- Implement a cooperative scheduler
scheduler(tasks, concurrency)that runs multiple async tasks with a limit on how many execute per event loop tick, yielding between ticks. - Build a progress bar that updates visually during a long computation by yielding to the event loop with
requestAnimationFrameorsetTimeout(0). Show actual percentage completion.
5. Mini Project: Event Loop Visualizer
Build an interactive visualizer that demonstrates the event loop's behavior with different task types.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Event Loop Visualizer</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: monospace;
background: #1e1e1e;
color: #d4d4d4;
max-width: 700px;
margin: 30px auto;
padding: 20px;
}
h1 { text-align: center; margin-bottom: 20px; color: #569cd6; }
.controls { display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; margin-bottom: 20px; }
.controls button {
padding: 10px 16px; border: none; border-radius: 4px;
cursor: pointer; font-family: monospace; font-size: 14px;
transition: transform 0.1s;
}
.controls button:hover { transform: scale(1.05); }
.controls button:active { transform: scale(0.95); }
.btn-sync { background: #4ec9b0; color: #000; }
.btn-micro { background: #c586c0; color: #000; }
.btn-macro { background: #dcdcaa; color: #000; }
.btn-raf { background: #ce9178; color: #000; }
.btn-clear { background: #f44747; color: #fff; }
.btn-block { background: #e06c75; color: #000; }
#output {
background: #252526;
border: 1px solid #3c3c3c;
border-radius: 4px;
padding: 15px;
min-height: 300px;
max-height: 500px;
overflow-y: auto;
font-size: 14px;
line-height: 1.6;
}
#output .log-entry { display: flex; gap: 10px; align-items: center; }
#output .time { color: #6a9955; font-size: 12px; }
#output .tag { padding: 2px 6px; border-radius: 3px; font-size: 11px; font-weight: bold; }
.tag-sync { background: #4ec9b0; color: #000; }
.tag-micro { background: #c586c0; color: #000; }
.tag-macro { background: #dcdcaa; color: #000; }
.tag-raf { background: #ce9178; color: #000; }
.tag-block { background: #e06c75; color: #000; }
.info { text-align: center; margin-top: 10px; color: #888; font-size: 13px; }
</style>
</head>
<body>
<h1>🔄 Event Loop Visualizer</h1>
<div class="controls">
<button class="btn-sync" id="btn-sync">➕ Sync</button>
<button class="btn-micro" id="btn-micro">➕ Microtask</button>
<button class="btn-macro" id="btn-macro">➕ Macrotask</button>
<button class="btn-raf" id="btn-raf">➕ rAF</button>
<button class="btn-block" id="btn-block">⚠️ Block (2s)</button>
<button class="btn-clear" id="btn-clear">🗑️ Clear</button>
</div>
<div id="output">
<div class="log-entry">
<span class="time">—</span>
<span>Click a button to see the event loop in action</span>
</div>
</div>
<div class="info">Check the console for additional timing details</div>
<script>
const output = document.getElementById("output");
let entryCount = 0;
function addLog(message, tagClass, tagText) {
entryCount++;
const now = new Date();
const time = now.toLocaleTimeString("en-US", { hour12: false }) + "." + String(now.getMilliseconds()).padStart(3, "0");
const div = document.createElement("div");
div.className = "log-entry";
div.innerHTML = `
<span class="time">${time}</span>
<span class="tag ${tagClass}">${tagText}</span>
<span>${message}</span>
`;
output.appendChild(div);
output.scrollTop = output.scrollHeight;
}
// Synchronous
document.getElementById("btn-sync").addEventListener("click", () => {
addLog("Synchronous code executed", "tag-sync", "SYNC");
});
// Microtask (Promise)
document.getElementById("btn-micro").addEventListener("click", () => {
addLog("Microtask queued (Promise)", "tag-micro", "QUEUE");
Promise.resolve().then(() => {
addLog("Microtask executed (Promise.then)", "tag-micro", "MICRO");
});
});
// Microtask (queueMicrotask)
document.getElementById("btn-micro").addEventListener("dblclick", () => {
addLog("Microtask queued (queueMicrotask)", "tag-micro", "QUEUE");
queueMicrotask(() => {
addLog("Microtask executed (queueMicrotask)", "tag-micro", "MICRO");
});
});
// Macrotask (setTimeout)
document.getElementById("btn-macro").addEventListener("click", () => {
addLog("Macrotask queued (setTimeout 0)", "tag-macro", "QUEUE");
setTimeout(() => {
addLog("Macrotask executed (setTimeout)", "tag-macro", "MACRO");
}, 0);
});
// requestAnimationFrame
document.getElementById("btn-raf").addEventListener("click", () => {
addLog("rAF queued", "tag-raf", "QUEUE");
requestAnimationFrame(() => {
addLog("rAF callback executed", "tag-raf", "rAF");
});
});
// Blocking
document.getElementById("btn-block").addEventListener("click", () => {
addLog("Blocking for 2 seconds...", "tag-block", "BLOCK");
console.time("Block");
const start = Date.now();
while (Date.now() - start < 2000) {
// busy wait
}
console.timeEnd("Block");
addLog("Block finished!", "tag-block", "BLOCK");
});
// Clear
document.getElementById("btn-clear").addEventListener("click", () => {
output.innerHTML = '<div class="log-entry"><span class="time">—</span><span>Cleared. Click a button to start.</span></div>';
entryCount = 0;
});
// Demo: queue multiple at once to see the order
document.addEventListener("dblclick", () => {
addLog("=== Running demo: sync → micro → macro ===", "tag-sync", "DEMO");
console.log("Sync 1");
setTimeout(() => {
addLog("Macro: setTimeout(0) ran", "tag-macro", "MACRO");
console.log("Macro");
}, 0);
Promise.resolve().then(() => {
addLog("Micro: Promise.then ran", "tag-micro", "MICRO");
console.log("Micro");
});
console.log("Sync 2");
addLog("Demo done (check console for order: sync, sync, micro, macro)", "tag-sync", "DEMO");
});
console.log("Event Loop Visualizer ready. Click buttons or double-click for demo.");
</script>
</body>
</html>
6. Common Mistakes
| Mistake | Why it's wrong | Correct approach |
|---|---|---|
| Blocking the event loop with long synchronous operations | Freezes the UI and prevents all other tasks from executing | Yield with setTimeout(0), queueMicrotask, or requestAnimationFrame |
Assuming setTimeout(fn, 0) runs immediately | Executes only after all sync code and microtasks are done | Understand that 0ms means "as soon as possible" but not immediately |
| Starving the microtask queue | If microtasks keep adding more microtasks, macrotasks never run | Limit microtask recursion or use macrotasks to break the chain |
Using for...of with await and expecting parallelism | Iterates sequentially, not in parallel | Use Promise.all() for concurrent operations |
Expecting requestAnimationFrame to run at a fixed interval | Runs when the browser is ready to repaint (typically 60fps) | Use setInterval for fixed intervals; use rAF for smooth animations |
Not understanding that Promise.resolve().then() is a microtask | then() callbacks run before the next macrotask, not immediately | Use this knowledge to control execution order |
| Creating deep Promise chains that starve rendering | All microtasks run before rAF and repaint, delaying visual updates | Insert setTimeout(0) or requestAnimationFrame to allow rendering |
7. Best Practices
- Avoid blocking the main thread — break large synchronous tasks into chunks and yield between them.
- Use
requestAnimationFramefor visual updates — it synchronizes with the browser's repaint cycle. - Use
Promise.all()for concurrent async operations rather than sequentialawait. - Use
queueMicrotask()sparingly — it can starve other tasks if overused. - Prefer
setTimeout(0)to yield control — it's a macrotask, so it allows microtasks and rendering to run. - Understand the execution order — sync → microtasks → requestAnimationFrame → render → macrotasks.
- Use Web Workers for CPU-intensive tasks — they run in a separate thread and don't block the UI.
- Limit Promise chain depth — deep chains can delay rendering.
- Test with realistic workloads — a simple demo might not reveal event loop issues.
- Use the browser's Performance tab to visualize event loop activity and frame drops.
8. Challenge Assignment (Optional)
"Cooperative Task Scheduler" Challenge
Build a task scheduler that runs multiple heavy computations while keeping the UI responsive.
Requirements:
Create a
Schedulerclass that:addTask(name, fn, priority)— adds a task function (returns a Promise)start()— begins processing tasks in the event loop's idle timestop()— pauses processinggetQueueLength()— returns pending task counton("taskComplete", callback)— event when a task finisheson("drain", callback)— event when all tasks are done
Each task should be broken into chunks that yield to the event loop using
setTimeout(0)orrequestIdleCallback.Support priority levels:
high,normal,low. High-priority tasks run first.Show a progress indicator in the DOM that updates as tasks complete.
Bonus:
- Add preemptive priority — a high-priority task can interrupt a running low-priority task
- Create a visual dashboard that shows task queue, currently running task, and completed tasks
- Implement task cancellation via an
AbortController
Example usage:
const scheduler = new Scheduler();
scheduler.addTask("Calculate primes", () => calculatePrimes(100000), "low");
scheduler.addTask("Process data", () => processLargeArray(data), "high");
scheduler.addTask("Generate report", () => generateReport(), "normal");
scheduler.on("taskComplete", (name) => console.log(`✅ ${name} done`));
scheduler.on("drain", () => console.log("All tasks complete"));
scheduler.start();
9. Knowledge Check (Quiz)
What is the correct order of execution in the event loop?
- a) Macrotasks → Microtasks → Sync → Render
- b) Sync → Microtasks → Render → Macrotasks
- c) Sync → Macrotasks → Microtasks → Render
- d) Microtasks → Sync → Macrotasks → Render
What will this code output?
console.log("A"); setTimeout(() => console.log("B"), 0); Promise.resolve().then(() => console.log("C")); console.log("D");- a) A, B, C, D
- b) A, D, C, B
- c) A, C, D, B
- d) A, D, B, C
Which queue has higher priority: microtasks or macrotasks?
- a) Microtasks
- b) Macrotasks
- c) They have equal priority
- d) It depends on the browser
What is the main problem with this code?
function process(items) { items.forEach((item, i) => { queueMicrotask(() => { console.log(i); if (i < items.length - 1) process(items.slice(i + 1)); }); }); }- a) It's recursive and causes a stack overflow
- b) It starves the microtask queue, potentially blocking macrotasks
- c) It's not async
- d)
queueMicrotaskdoesn't exist
When does
requestAnimationFramecallback execute?- a) Immediately after being queued
- b) After all microtasks but before the next paint
- c) In the next macrotask
- d) At a random interval
How can you yield control to allow other tasks to run during a long computation?
- a) Use a
whileloop with a break condition - b) Use
setTimeout(0)orqueueMicrotaskto schedule the next chunk - c) Use
Promise.resolve().then()recursively - d) Both b and c can work, but b is safer
- a) Use a
What does
process.nextTick()do in Node.js?- a) Schedules a macrotask
- b) Schedules a microtask with higher priority than Promise callbacks
- c) Runs immediately, blocking the event loop
- d) Schedules a callback in the next event loop iteration
How can you run CPU-intensive work without blocking the UI?
- a) Use
setTimeoutto split work into small chunks - b) Use a Web Worker
- c) Use
requestIdleCallback - d) All of the above
- a) Use
Answers: 1-b, 2-b (A sync, D sync, C microtask, B macrotask), 3-a, 4-b (microtask starvation), 5-b, 6-d, 7-b, 8-d
10. Additional Resources
- MDN: Event Loop → developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop
- MDN: Concurrency model → developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop#event_loop
- MDN: In depth: Microtasks and the JavaScript runtime environment → developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide/In_depth
- JavaScript.info: Event Loop → javascript.info/event-loop
- JavaScript.info: Microtasks and macrotasks → javascript.info/microtask-queue
- Video: "What the heck is the event loop anyway?" by Philip Roberts (JSConf EU)
- Video: "In the Loop" by Jake Archibald (JSConf.Asia)
- Video: "JavaScript Event Loop" by Web Dev Simplified
- Practice: loupe — Event Loop Visualizer by Philip Roberts
- Practice: JSV9000 — Event Loop Visualizer
- Book: Chapter 11 of Eloquent JavaScript — "Asynchronous Programming"
11. Summary
Today you learned:
- JavaScript is single-threaded but uses an event loop to handle asynchronous operations
- The call stack executes synchronous code; long operations block it
- Microtasks (Promise callbacks, queueMicrotask) have higher priority than macrotasks (setTimeout, setInterval, I/O)
- requestAnimationFrame runs before the browser repaints, ideal for animations
- Blocking the event loop freezes the UI; use chunking and yielding to avoid it
- Starving the microtask queue prevents macrotasks from running; limit microtask recursion
- Cooperative yielding with
setTimeout(0)orqueueMicrotaskkeeps the UI responsive - Web Workers provide true parallelism for CPU-intensive tasks
You now have a deep understanding of how JavaScript manages concurrency — essential knowledge for building responsive, performant applications. Tomorrow we'll explore performance and memory optimization.
"The event loop is the heartbeat of JavaScript. Understanding its rhythm is the key to writing code that is both responsive and efficient."

Comments
Post a Comment