JavaScript Event Loop - Complete Guide

The event loop is the mechanism that allows JavaScript to perform non-blocking operations. This guide explains how it works in detail.

What is the Event Loop? #

JavaScript is single-threaded but can handle asynchronous operations through the event loop. It coordinates execution between the call stack, task queues, and Web APIs.

Call Stack #

The call stack tracks function execution.

function first() {
  console.log("First");
  second();
  console.log("First again");
}

function second() {
  console.log("Second");
}

first();

// Call Stack Timeline:
// 1. first() added
// 2. console.log("First") executed
// 3. second() added
// 4. console.log("Second") executed
// 5. second() removed
// 6. console.log("First again") executed
// 7. first() removed

// Output:
// First
// Second
// First again

Web APIs #

Browser provides APIs for async operations:

  • setTimeout, setInterval
  • DOM events
  • fetch, XMLHttpRequest
  • Promises
console.log("Start");

setTimeout(() => {
  console.log("Timeout");
}, 0);

console.log("End");

// Output:
// Start
// End
// Timeout

Task Queues #

Macrotask Queue (Task Queue) #

Contains: setTimeout, setInterval, I/O, UI rendering

console.log("1");

setTimeout(() => {
  console.log("2");
}, 0);

console.log("3");

// Output: 1, 3, 2

Microtask Queue #

Contains: Promises, queueMicrotask, MutationObserver

console.log("1");

setTimeout(() => {
  console.log("2");
}, 0);

Promise.resolve().then(() => {
  console.log("3");
});

console.log("4");

// Output: 1, 4, 3, 2
// Microtasks run before macrotasks

Event Loop Execution Order #

  1. Execute synchronous code (call stack)
  2. Execute all microtasks
  3. Execute one macrotask
  4. Repeat from step 2
console.log("1");  // Synchronous

setTimeout(() => {
  console.log("2");  // Macrotask
}, 0);

Promise.resolve()
  .then(() => console.log("3"))  // Microtask
  .then(() => console.log("4")); // Microtask

setTimeout(() => {
  console.log("5");  // Macrotask
}, 0);

console.log("6");  // Synchronous

// Output: 1, 6, 3, 4, 2, 5

Complex Example #

console.log("Start");

setTimeout(() => {
  console.log("setTimeout 1");
  Promise.resolve().then(() => console.log("Promise in setTimeout"));
}, 0);

Promise.resolve()
  .then(() => {
    console.log("Promise 1");
    setTimeout(() => console.log("setTimeout in Promise"), 0);
  })
  .then(() => console.log("Promise 2"));

setTimeout(() => {
  console.log("setTimeout 2");
}, 0);

console.log("End");

// Output:
// Start
// End
// Promise 1
// Promise 2
// setTimeout 1
// Promise in setTimeout
// setTimeout 2
// setTimeout in Promise

Async/Await and Event Loop #

async function example() {
  console.log("1");

  await Promise.resolve();

  console.log("2");
}

console.log("3");

example();

console.log("4");

// Output: 3, 1, 4, 2
// await pauses function, continues after current sync code

SetTimeout vs SetImmediate vs Process.nextTick #

In Node.js: #

setTimeout(() => console.log("setTimeout"), 0);
setImmediate(() => console.log("setImmediate"));
process.nextTick(() => console.log("nextTick"));

console.log("sync");

// Output:
// sync
// nextTick
// setTimeout or setImmediate (order varies)

Priority (Node.js): #

  1. process.nextTick (highest)
  2. Promises/microtasks
  3. setTimeout/setInterval
  4. setImmediate
  5. I/O callbacks

RequestAnimationFrame #

Runs before next repaint (browser-only):

console.log("1");

requestAnimationFrame(() => {
  console.log("2 - RAF");
});

setTimeout(() => {
  console.log("3 - setTimeout");
}, 0);

Promise.resolve().then(() => {
  console.log("4 - Promise");
});

console.log("5");

// Output: 1, 5, 4, 2, 3

Common Patterns #

Avoiding Stack Overflow #

// Synchronous - causes stack overflow
function processArray(items) {
  items.forEach(item => {
    processItem(item);
  });
}

// Asynchronous - spreads work across event loop
async function processArray(items) {
  for (const item of items) {
    await processItem(item);
    await new Promise(resolve => setTimeout(resolve, 0));
  }
}

Breaking Up Long Tasks #

function heavyComputation(data) {
  return new Promise(resolve => {
    const batchSize = 1000;
    let index = 0;

    function processBatch() {
      const end = Math.min(index + batchSize, data.length);

      for (let i = index; i < end; i++) {
        // Process data[i]
      }

      index = end;

      if (index < data.length) {
        setTimeout(processBatch, 0);
      } else {
        resolve();
      }
    }

    processBatch();
  });
}

Debouncing with Event Loop #

function debounce(fn, delay) {
  let timeoutId;

  return function(...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => fn(...args), delay);
  };
}

const search = debounce(query => {
  console.log("Searching:", query);
}, 300);

Browser vs Node.js Event Loop #

Browser: #

  • Render steps between macrotasks
  • requestAnimationFrame before render
  • MutationObserver in microtask queue

Node.js: #

  • Multiple phases (timers, I/O callbacks, idle, poll, check, close)
  • process.nextTick has highest priority
  • setImmediate in check phase

Debugging Event Loop Issues #

Visualizing with Timers #

console.time("Task");

setTimeout(() => {
  console.timeEnd("Task");
}, 100);

// Shows actual time taken

Finding Blocking Code #

const start = Date.now();

// Blocking operation
for (let i = 0; i < 1000000000; i++) {}

console.log("Blocked for:", Date.now() - start, "ms");

Using Performance API #

performance.mark("start");

setTimeout(() => {
  performance.mark("end");
  performance.measure("task", "start", "end");

  const measure = performance.getEntriesByName("task")[0];
  console.log("Duration:", measure.duration);
}, 100);

Common Pitfalls #

Pitfall 1: Assuming Immediate Execution #

// Wrong assumption
setTimeout(() => {
  console.log("Runs immediately");
}, 0);  // Actually waits for event loop

console.log("First");
// Output: First, Runs immediately

Pitfall 2: Promise Constructor Executor #

new Promise((resolve) => {
  console.log("1");  // Runs immediately!
  resolve();
}).then(() => {
  console.log("2");
});

console.log("3");

// Output: 1, 3, 2

Pitfall 3: Mixing Callbacks and Promises #

function getData(callback) {
  setTimeout(() => {
    callback("data");
  }, 0);
}

// Callback runs after promise
Promise.resolve().then(() => console.log("Promise"));
getData(data => console.log(data));

// Output: Promise, data

Real-World Examples #

Implementing Sleep #

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function example() {
  console.log("Start");
  await sleep(1000);
  console.log("After 1 second");
}

Rate Limiting with Event Loop #

async function rateLimitedFetch(urls, requestsPerSecond) {
  const delay = 1000 / requestsPerSecond;
  const results = [];

  for (const url of urls) {
    const result = await fetch(url);
    results.push(result);
    await new Promise(resolve => setTimeout(resolve, delay));
  }

  return results;
}

Chunked Array Processing #

async function processLargeArray(array, processor) {
  const chunkSize = 100;

  for (let i = 0; i < array.length; i += chunkSize) {
    const chunk = array.slice(i, i + chunkSize);

    for (const item of chunk) {
      processor(item);
    }

    // Yield to event loop
    await new Promise(resolve => setTimeout(resolve, 0));
  }
}

Interview Questions #

Q: What will this code output?

console.log("1");

setTimeout(() => console.log("2"), 0);

Promise.resolve().then(() => console.log("3"));

console.log("4");

// Answer: 1, 4, 3, 2

Q: Why does this happen?

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}

// Output: 3, 3, 3
// Because var is function-scoped, all callbacks share same 'i'

Q: How to fix it?

// Solution 1: let
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}

// Solution 2: IIFE
for (var i = 0; i < 3; i++) {
  (function(j) {
    setTimeout(() => console.log(j), 0);
  })(i);
}

Best Practices #

  1. Don’t block the event loop - Break up long tasks
  2. Understand microtask priority - Promises before setTimeout
  3. Use async/await for cleaner async code
  4. Avoid nested callbacks - Use promises or async/await
  5. Monitor performance - Profile blocking operations
  6. Test async behavior - Don’t assume execution order

Understanding the event loop is crucial for writing efficient JavaScript. It explains why async code behaves the way it does and helps you write better concurrent programs.