Back to Blog
JavaScriptEvent LoopAsyncFundamentals

Understanding the JavaScript Event Loop

Master the JavaScript event loop. From call stack to task queues to microtasks and how they affect your code.

B
Bootspring Team
Engineering
February 24, 2022
6 min read

The event loop is fundamental to JavaScript's asynchronous behavior. Here's how it works and why it matters.

The Call Stack#

1// JavaScript is single-threaded 2// One call stack, one thing at a time 3 4function multiply(a, b) { 5 return a * b; 6} 7 8function square(n) { 9 return multiply(n, n); 10} 11 12function printSquare(n) { 13 const result = square(n); 14 console.log(result); 15} 16 17printSquare(4); 18 19// Call stack during execution: 20// 1. printSquare(4) 21// 2. printSquare(4) -> square(4) 22// 3. printSquare(4) -> square(4) -> multiply(4, 4) 23// 4. printSquare(4) -> square(4) (multiply returns 16) 24// 5. printSquare(4) (square returns 16) 25// 6. (printSquare returns, stack empty)

Blocking vs Non-Blocking#

1// Blocking code - stops everything 2function blockingOperation() { 3 const start = Date.now(); 4 while (Date.now() - start < 3000) { 5 // Blocks for 3 seconds 6 } 7 console.log('Done blocking'); 8} 9 10// Non-blocking - uses callbacks 11function nonBlockingOperation() { 12 setTimeout(() => { 13 console.log('Done after timeout'); 14 }, 3000); 15} 16 17console.log('Start'); 18nonBlockingOperation(); 19console.log('End'); 20 21// Output: 22// Start 23// End 24// Done after timeout (3 seconds later)

Task Queue (Macrotasks)#

1// setTimeout, setInterval, I/O, UI rendering 2// These go to the task queue (macrotask queue) 3 4console.log('1'); 5 6setTimeout(() => { 7 console.log('2'); 8}, 0); 9 10console.log('3'); 11 12// Output: 1, 3, 2 13// Even with 0ms delay, setTimeout is queued 14 15// Each task queue iteration processes ONE macrotask 16setTimeout(() => console.log('timeout 1'), 0); 17setTimeout(() => console.log('timeout 2'), 0); 18setTimeout(() => console.log('timeout 3'), 0); 19 20// Each executes in separate event loop iterations

Microtasks#

1// Promises, queueMicrotask, MutationObserver 2// Microtasks run AFTER current task, BEFORE next macrotask 3 4console.log('1'); 5 6setTimeout(() => { 7 console.log('2 - macrotask'); 8}, 0); 9 10Promise.resolve().then(() => { 11 console.log('3 - microtask'); 12}); 13 14console.log('4'); 15 16// Output: 1, 4, 3 - microtask, 2 - macrotask 17// Microtask (Promise) runs before macrotask (setTimeout) 18 19// All microtasks are processed before any macrotask 20Promise.resolve().then(() => console.log('micro 1')); 21Promise.resolve().then(() => console.log('micro 2')); 22Promise.resolve().then(() => console.log('micro 3')); 23 24setTimeout(() => console.log('macro 1'), 0); 25 26// Output: micro 1, micro 2, micro 3, macro 1

Event Loop Order#

1// Event loop priority: 2// 1. Execute synchronous code (call stack) 3// 2. Execute all microtasks 4// 3. Execute one macrotask 5// 4. Repeat 6 7console.log('sync 1'); 8 9setTimeout(() => { 10 console.log('macro 1'); 11 Promise.resolve().then(() => console.log('micro inside macro')); 12}, 0); 13 14Promise.resolve().then(() => { 15 console.log('micro 1'); 16 setTimeout(() => console.log('macro inside micro'), 0); 17}); 18 19setTimeout(() => console.log('macro 2'), 0); 20 21console.log('sync 2'); 22 23// Output: 24// sync 1 25// sync 2 26// micro 1 27// macro 1 28// micro inside macro 29// macro 2 30// macro inside micro

Async/Await and the Event Loop#

1// async/await is syntactic sugar over Promises 2// Same microtask behavior 3 4async function asyncFunc() { 5 console.log('async start'); 6 await Promise.resolve(); 7 console.log('after await'); 8} 9 10console.log('1'); 11asyncFunc(); 12console.log('2'); 13 14// Output: 1, async start, 2, after await 15// Code after await is scheduled as microtask 16 17// Multiple awaits 18async function multipleAwaits() { 19 console.log('A'); 20 await Promise.resolve(); 21 console.log('B'); 22 await Promise.resolve(); 23 console.log('C'); 24} 25 26console.log('1'); 27multipleAwaits(); 28console.log('2'); 29 30// Output: 1, A, 2, B, C

queueMicrotask#

1// Explicitly queue a microtask 2queueMicrotask(() => { 3 console.log('microtask'); 4}); 5 6console.log('sync'); 7 8// Output: sync, microtask 9 10// Use case: Defer work but run before rendering 11function updateUI(changes) { 12 // Batch updates in microtask 13 queueMicrotask(() => { 14 applyChanges(changes); 15 }); 16} 17 18// Multiple calls batch naturally 19updateUI({ a: 1 }); 20updateUI({ b: 2 }); 21// Both applied in same microtask batch

requestAnimationFrame#

1// requestAnimationFrame runs before repaint 2// After macrotasks and microtasks, before paint 3 4console.log('sync'); 5 6requestAnimationFrame(() => { 7 console.log('rAF'); 8}); 9 10setTimeout(() => { 11 console.log('timeout'); 12}, 0); 13 14Promise.resolve().then(() => { 15 console.log('microtask'); 16}); 17 18// Typical output: sync, microtask, timeout, rAF 19// (rAF timing depends on refresh rate) 20 21// Use for animations 22function animate() { 23 updatePosition(); 24 requestAnimationFrame(animate); 25}

Node.js Event Loop#

1// Node.js has additional phases 2// process.nextTick and setImmediate 3 4// process.nextTick - runs BEFORE microtasks 5process.nextTick(() => console.log('nextTick')); 6Promise.resolve().then(() => console.log('promise')); 7// Output: nextTick, promise 8 9// setImmediate - runs after I/O callbacks 10setTimeout(() => console.log('timeout'), 0); 11setImmediate(() => console.log('immediate')); 12 13// Order depends on context: 14// In main script: either order 15// In I/O callback: immediate always first 16 17const fs = require('fs'); 18 19fs.readFile('file.txt', () => { 20 setTimeout(() => console.log('timeout'), 0); 21 setImmediate(() => console.log('immediate')); 22}); 23// Output: immediate, timeout (guaranteed)

Common Pitfalls#

1// Pitfall 1: Starving the event loop 2async function starve() { 3 while (true) { 4 await Promise.resolve(); 5 // Infinite microtask loop - blocks macrotasks! 6 } 7} 8 9// Pitfall 2: Expecting synchronous behavior 10let result = 'initial'; 11 12Promise.resolve().then(() => { 13 result = 'updated'; 14}); 15 16console.log(result); // 'initial', not 'updated' 17 18// Pitfall 3: setTimeout ordering assumptions 19for (let i = 0; i < 3; i++) { 20 setTimeout(() => console.log(i), 0); 21} 22// Output: 0, 1, 2 (as expected with let) 23// With var: 3, 3, 3 (closure issue) 24 25// Pitfall 4: Heavy computation blocking 26function processData(data) { 27 // Breaks up work to not block 28 const CHUNK_SIZE = 1000; 29 30 function processChunk(start) { 31 const end = Math.min(start + CHUNK_SIZE, data.length); 32 33 for (let i = start; i < end; i++) { 34 heavyComputation(data[i]); 35 } 36 37 if (end < data.length) { 38 setTimeout(() => processChunk(end), 0); 39 } 40 } 41 42 processChunk(0); 43}

Debugging the Event Loop#

1// Visualize task timing 2function logWithTime(label) { 3 console.log(`${Date.now() % 10000}: ${label}`); 4} 5 6logWithTime('sync 1'); 7 8setTimeout(() => logWithTime('macro 1'), 0); 9setTimeout(() => logWithTime('macro 2'), 0); 10 11Promise.resolve() 12 .then(() => logWithTime('micro 1')) 13 .then(() => logWithTime('micro 2')); 14 15queueMicrotask(() => logWithTime('queueMicrotask')); 16 17logWithTime('sync 2'); 18 19// Use Chrome DevTools Performance tab 20// to visualize event loop activity 21 22// Check for long tasks 23const observer = new PerformanceObserver((list) => { 24 for (const entry of list.getEntries()) { 25 console.log('Long task:', entry.duration); 26 } 27}); 28 29observer.observe({ entryTypes: ['longtask'] });

Best Practices#

Understanding: ✓ Microtasks before macrotasks ✓ All microtasks drain before next macro ✓ Heavy sync code blocks the loop ✓ Promises are microtasks Performance: ✓ Break up heavy computation ✓ Use requestAnimationFrame for animation ✓ Avoid infinite microtask loops ✓ Use Web Workers for CPU-intensive work Debugging: ✓ Log timing to understand order ✓ Use Performance DevTools ✓ Monitor for long tasks ✓ Test edge cases with timeouts

Conclusion#

The event loop enables JavaScript's asynchronous behavior while remaining single-threaded. Understanding the distinction between macrotasks and microtasks, and how they're processed, helps you write predictable async code and avoid common pitfalls like blocking the event loop.

Share this article

Help spread the word about Bootspring