The event loop enables JavaScript's asynchronous, non-blocking behavior. Here's how it works.
The Basics#
1// JavaScript is single-threaded
2// Only one piece of code runs at a time
3
4console.log('1'); // Runs first
5
6setTimeout(() => {
7 console.log('2'); // Runs last (after delay)
8}, 0);
9
10console.log('3'); // Runs second
11
12// Output: 1, 3, 2
13// Even with 0ms delay, setTimeout callback waitsCall Stack#
1// The call stack tracks function execution
2
3function first() {
4 console.log('first');
5 second();
6}
7
8function second() {
9 console.log('second');
10 third();
11}
12
13function third() {
14 console.log('third');
15}
16
17first();
18
19// Call stack progression:
20// 1. first() pushed
21// 2. console.log('first') pushed, executed, popped
22// 3. second() pushed
23// 4. console.log('second') pushed, executed, popped
24// 5. third() pushed
25// 6. console.log('third') pushed, executed, popped
26// 7. third() popped
27// 8. second() popped
28// 9. first() popped
29
30// Stack overflow example
31function recursive() {
32 recursive(); // Eventually causes: Maximum call stack size exceeded
33}Task Queue (Macrotasks)#
1// Macrotasks include:
2// - setTimeout
3// - setInterval
4// - setImmediate (Node.js)
5// - I/O operations
6// - UI rendering events
7
8console.log('Start');
9
10setTimeout(() => console.log('Timeout 1'), 0);
11setTimeout(() => console.log('Timeout 2'), 0);
12
13console.log('End');
14
15// Output: Start, End, Timeout 1, Timeout 2
16
17// One macrotask per event loop iteration
18setTimeout(() => {
19 console.log('Macro 1');
20 setTimeout(() => console.log('Macro 2'), 0);
21}, 0);
22
23setTimeout(() => console.log('Macro 3'), 0);
24
25// Output: Macro 1, Macro 3, Macro 2
26// Each setTimeout callback is a separate taskMicrotask Queue#
1// Microtasks include:
2// - Promise.then/catch/finally
3// - queueMicrotask()
4// - MutationObserver
5// - process.nextTick (Node.js)
6
7console.log('Start');
8
9Promise.resolve().then(() => console.log('Microtask 1'));
10Promise.resolve().then(() => console.log('Microtask 2'));
11
12console.log('End');
13
14// Output: Start, End, Microtask 1, Microtask 2
15
16// All microtasks run before next macrotask
17setTimeout(() => console.log('Timeout'), 0);
18
19Promise.resolve().then(() => {
20 console.log('Microtask 1');
21 Promise.resolve().then(() => console.log('Microtask 2'));
22});
23
24console.log('Script');
25
26// Output: Script, Microtask 1, Microtask 2, TimeoutExecution Order#
1console.log('1 - Script start');
2
3setTimeout(() => {
4 console.log('2 - setTimeout');
5}, 0);
6
7Promise.resolve()
8 .then(() => {
9 console.log('3 - Promise 1');
10 })
11 .then(() => {
12 console.log('4 - Promise 2');
13 });
14
15queueMicrotask(() => {
16 console.log('5 - queueMicrotask');
17});
18
19console.log('6 - Script end');
20
21// Output order:
22// 1 - Script start (synchronous)
23// 6 - Script end (synchronous)
24// 3 - Promise 1 (microtask)
25// 5 - queueMicrotask (microtask)
26// 4 - Promise 2 (microtask, queued by previous)
27// 2 - setTimeout (macrotask)
28
29// The algorithm:
30// 1. Execute all synchronous code
31// 2. Process ALL microtasks (including newly added ones)
32// 3. Render (if needed)
33// 4. Execute ONE macrotask
34// 5. Go to step 2Async/Await Behavior#
1async function foo() {
2 console.log('foo start');
3 await bar();
4 console.log('foo end'); // This becomes a microtask
5}
6
7async function bar() {
8 console.log('bar');
9}
10
11console.log('script start');
12foo();
13console.log('script end');
14
15// Output:
16// script start
17// foo start
18// bar
19// script end
20// foo end
21
22// await is syntactic sugar for:
23function fooDesugared() {
24 console.log('foo start');
25 return Promise.resolve(bar()).then(() => {
26 console.log('foo end');
27 });
28}
29
30// Complex example
31async function async1() {
32 console.log('async1 start');
33 await async2();
34 console.log('async1 end');
35}
36
37async function async2() {
38 console.log('async2');
39}
40
41console.log('script start');
42
43setTimeout(() => {
44 console.log('setTimeout');
45}, 0);
46
47async1();
48
49new Promise((resolve) => {
50 console.log('promise1');
51 resolve();
52}).then(() => {
53 console.log('promise2');
54});
55
56console.log('script end');
57
58// Output:
59// script start
60// async1 start
61// async2
62// promise1
63// script end
64// async1 end
65// promise2
66// setTimeoutBlocking the Event Loop#
1// Bad: Blocking synchronous code
2function heavyComputation() {
3 const start = Date.now();
4 while (Date.now() - start < 3000) {
5 // Blocks for 3 seconds
6 // No other code can run
7 // UI becomes unresponsive
8 }
9}
10
11// Good: Break up work with setTimeout
12function nonBlockingComputation(items, callback) {
13 const chunkSize = 100;
14 let index = 0;
15
16 function processChunk() {
17 const end = Math.min(index + chunkSize, items.length);
18
19 for (; index < end; index++) {
20 // Process item
21 }
22
23 if (index < items.length) {
24 setTimeout(processChunk, 0); // Yield to event loop
25 } else {
26 callback();
27 }
28 }
29
30 processChunk();
31}
32
33// Better: Use requestIdleCallback for non-urgent work
34function processInIdle(items, callback) {
35 let index = 0;
36
37 function process(deadline) {
38 while (index < items.length && deadline.timeRemaining() > 0) {
39 // Process items while we have time
40 index++;
41 }
42
43 if (index < items.length) {
44 requestIdleCallback(process);
45 } else {
46 callback();
47 }
48 }
49
50 requestIdleCallback(process);
51}requestAnimationFrame#
1// Runs before repaint (approximately 60fps)
2// Special timing: between microtasks and next frame
3
4function animate() {
5 console.log('Animation frame');
6
7 // Schedule next frame
8 requestAnimationFrame(animate);
9}
10
11requestAnimationFrame(animate);
12
13// Comparison
14console.log('Start');
15
16requestAnimationFrame(() => console.log('rAF'));
17setTimeout(() => console.log('Timeout'), 0);
18Promise.resolve().then(() => console.log('Promise'));
19
20console.log('End');
21
22// Output: Start, End, Promise, rAF, Timeout
23// (rAF may vary based on timing)
24
25// Smooth animations
26let position = 0;
27
28function moveElement() {
29 position += 1;
30 element.style.transform = `translateX(${position}px)`;
31
32 if (position < 300) {
33 requestAnimationFrame(moveElement);
34 }
35}
36
37requestAnimationFrame(moveElement);Node.js Specifics#
1// Node.js has additional phases
2// Timers -> Pending -> Idle -> Poll -> Check -> Close
3
4// process.nextTick runs after current operation
5// Before any I/O, before microtasks
6
7process.nextTick(() => console.log('nextTick'));
8Promise.resolve().then(() => console.log('Promise'));
9setTimeout(() => console.log('Timeout'), 0);
10setImmediate(() => console.log('Immediate'));
11
12// Output:
13// nextTick
14// Promise
15// Timeout (may vary)
16// Immediate (may vary)
17
18// setImmediate vs setTimeout(fn, 0)
19// In I/O callback: setImmediate always first
20const fs = require('fs');
21
22fs.readFile('file.txt', () => {
23 setTimeout(() => console.log('Timeout'), 0);
24 setImmediate(() => console.log('Immediate'));
25});
26// Output: Immediate, Timeout (consistent)Common Pitfalls#
1// Pitfall 1: Assuming setTimeout(fn, 0) is instant
2for (var i = 0; i < 3; i++) {
3 setTimeout(() => console.log(i), 0);
4}
5// Output: 3, 3, 3 (var is function-scoped)
6
7// Fix: Use let
8for (let i = 0; i < 3; i++) {
9 setTimeout(() => console.log(i), 0);
10}
11// Output: 0, 1, 2
12
13// Pitfall 2: Starvation
14function starve() {
15 Promise.resolve().then(starve);
16}
17starve();
18// Microtask queue never empties
19// setTimeout callbacks never run
20
21// Pitfall 3: UI freezing
22button.addEventListener('click', () => {
23 heavyTask(); // Blocks UI
24 updateUI(); // User sees nothing until done
25});
26
27// Better
28button.addEventListener('click', async () => {
29 showLoading();
30 await new Promise(r => setTimeout(r, 0));
31 heavyTask();
32 hideLoading();
33 updateUI();
34});Debugging Tips#
1// Visualize execution order
2function trace(name) {
3 console.log(name);
4 console.trace();
5}
6
7// Use performance API
8const start = performance.now();
9// ... code ...
10const end = performance.now();
11console.log(`Took ${end - start}ms`);
12
13// Check if code is blocking
14const CHECK_INTERVAL = 100;
15let lastCheck = Date.now();
16
17setInterval(() => {
18 const now = Date.now();
19 const delay = now - lastCheck - CHECK_INTERVAL;
20 if (delay > 50) {
21 console.warn(`Event loop blocked for ${delay}ms`);
22 }
23 lastCheck = now;
24}, CHECK_INTERVAL);Best Practices#
Performance:
✓ Avoid blocking synchronous code
✓ Break up heavy computation
✓ Use Web Workers for CPU-intensive tasks
✓ Batch DOM updates
Understanding:
✓ Microtasks run to completion
✓ One macrotask per iteration
✓ rAF before repaint
✓ await creates microtask boundaries
Patterns:
✓ Use queueMicrotask for urgent async
✓ Use setTimeout(fn, 0) to yield
✓ Use requestIdleCallback for low priority
✓ Use rAF for animations
Debugging:
✓ Console.log execution order
✓ Use browser performance tools
✓ Check for stack traces
✓ Monitor event loop lag
Conclusion#
The event loop enables asynchronous JavaScript through task queues. Synchronous code runs first, then all microtasks, then one macrotask, then render if needed. Understanding this helps write performant, non-blocking code and debug timing issues.