Skip to main content
โšก Calmops

Event Loop and Microtasks in JavaScript

Event Loop and Microtasks in JavaScript

Introduction

The event loop is the heart of JavaScript’s asynchronous execution model. Understanding how the event loop works, along with microtasks and macrotasks, is essential for writing efficient asynchronous code and debugging timing-related issues. In this article, you’ll learn how JavaScript executes code, manages different types of tasks, and optimizes performance.

Understanding the Event Loop

What is the Event Loop?

The event loop is a mechanism that allows JavaScript to perform non-blocking operations despite being single-threaded. It continuously checks for tasks to execute and manages the execution order.

// Simple example showing event loop behavior
console.log('Start');

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

console.log('End');

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

The Call Stack

// Call stack example
function first() {
  console.log('First');
  second();
}

function second() {
  console.log('Second');
  third();
}

function third() {
  console.log('Third');
}

first();

// Output:
// First
// Second
// Third

// Call stack visualization:
// 1. first() pushed to stack
// 2. second() pushed to stack
// 3. third() pushed to stack
// 4. third() completes, popped from stack
// 5. second() completes, popped from stack
// 6. first() completes, popped from stack

Microtasks vs Macrotasks

Understanding the Difference

// Microtasks: Promises, MutationObserver, queueMicrotask()
// Macrotasks: setTimeout, setInterval, setImmediate, I/O

console.log('Script start');

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

Promise.resolve()
  .then(() => {
    console.log('Promise 1');
  })
  .then(() => {
    console.log('Promise 2');
  });

console.log('Script end');

// Output:
// Script start
// Script end
// Promise 1
// Promise 2
// setTimeout

Microtask Queue

// Microtasks execute before macrotasks
console.log('Start');

// Macrotask
setTimeout(() => {
  console.log('Macrotask 1');
}, 0);

// Microtask
Promise.resolve()
  .then(() => {
    console.log('Microtask 1');
  });

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

// Microtask
Promise.resolve()
  .then(() => {
    console.log('Microtask 2');
  });

console.log('End');

// Output:
// Start
// End
// Microtask 1
// Microtask 2
// Macrotask 1
// Macrotask 2

Macrotask Queue

// Macrotasks are processed one at a time
console.log('Start');

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

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

console.log('End');

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

Event Loop Execution Order

Complete Execution Flow

// 1. Execute all synchronous code (call stack)
// 2. Execute all microtasks (promises, queueMicrotask)
// 3. Render (if needed)
// 4. Execute one macrotask (setTimeout, setInterval)
// 5. Go back to step 2

console.log('1. Synchronous');

setTimeout(() => {
  console.log('2. Macrotask (setTimeout)');
  Promise.resolve().then(() => console.log('3. Microtask in Macrotask'));
}, 0);

Promise.resolve()
  .then(() => {
    console.log('4. Microtask (Promise)');
    setTimeout(() => {
      console.log('5. Macrotask in Microtask');
    }, 0);
  });

console.log('6. Synchronous');

// Output:
// 1. Synchronous
// 6. Synchronous
// 4. Microtask (Promise)
// 2. Macrotask (setTimeout)
// 3. Microtask in Macrotask
// 5. Macrotask in Microtask

Microtasks in Detail

Promise Microtasks

// Promises create microtasks
console.log('Start');

Promise.resolve()
  .then(() => console.log('Promise 1'))
  .then(() => console.log('Promise 2'))
  .then(() => console.log('Promise 3'));

console.log('End');

// Output:
// Start
// End
// Promise 1
// Promise 2
// Promise 3

queueMicrotask()

// queueMicrotask() explicitly queues a microtask
console.log('Start');

queueMicrotask(() => {
  console.log('Microtask 1');
});

Promise.resolve().then(() => {
  console.log('Microtask 2');
});

queueMicrotask(() => {
  console.log('Microtask 3');
});

console.log('End');

// Output:
// Start
// End
// Microtask 1
// Microtask 2
// Microtask 3

MutationObserver

// MutationObserver creates microtasks
console.log('Start');

const observer = new MutationObserver(() => {
  console.log('DOM changed');
});

const element = document.createElement('div');
observer.observe(element, { attributes: true });

element.setAttribute('data-test', 'value');

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

console.log('End');

// Output:
// Start
// End
// DOM changed
// Promise

Macrotasks in Detail

setTimeout and setInterval

// setTimeout creates macrotasks
console.log('Start');

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

setTimeout(() => {
  console.log('Timeout 2');
}, 10);

console.log('End');

// Output:
// Start
// End
// Timeout 1
// Timeout 2

setImmediate (Node.js)

// setImmediate creates macrotasks (Node.js only)
console.log('Start');

setImmediate(() => {
  console.log('Immediate 1');
});

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

setImmediate(() => {
  console.log('Immediate 2');
});

console.log('End');

// Output (Node.js):
// Start
// End
// Promise
// Immediate 1
// Immediate 2

Practical Event Loop Examples

Example 1: Complex Execution Order

// Complex example showing event loop behavior
console.log('Script start');

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

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

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

console.log('Script end');

// Output:
// Script start
// Script end
// Promise 1
// Promise 2
// setTimeout 1
// Promise in setTimeout 1
// setTimeout 2
// setTimeout in Promise 1

Example 2: Rendering and Event Loop

// Event loop with rendering
console.log('Start');

// This will block the main thread
const start = performance.now();
while (performance.now() - start < 100) {
  // Busy wait for 100ms
}

console.log('After blocking');

// Rendering happens here if needed

setTimeout(() => {
  console.log('After rendering');
}, 0);

// Output:
// Start
// After blocking
// After rendering

Example 3: Task Starvation

// Microtasks can starve macrotasks
function recursiveMicrotask() {
  console.log('Microtask');
  
  Promise.resolve().then(() => {
    recursiveMicrotask();
  });
}

recursiveMicrotask();

setTimeout(() => {
  console.log('This might never run!');
}, 0);

// The setTimeout will be delayed indefinitely
// because microtasks keep the event loop busy

Performance Implications

Microtasks vs Macrotasks Performance

// Microtasks are faster (execute sooner)
console.time('Microtask');
Promise.resolve().then(() => {
  console.timeEnd('Microtask');
});

console.time('Macrotask');
setTimeout(() => {
  console.timeEnd('Macrotask');
}, 0);

// Microtask is typically faster

Batch Updates with Microtasks

// Use microtasks for batching updates
class BatchUpdater {
  constructor() {
    this.updates = [];
    this.scheduled = false;
  }
  
  schedule(update) {
    this.updates.push(update);
    
    if (!this.scheduled) {
      this.scheduled = true;
      
      Promise.resolve().then(() => {
        this.flush();
      });
    }
  }
  
  flush() {
    const updates = this.updates;
    this.updates = [];
    this.scheduled = false;
    
    console.log(`Processing ${updates.length} updates`);
    updates.forEach(update => update());
  }
}

const updater = new BatchUpdater();

updater.schedule(() => console.log('Update 1'));
updater.schedule(() => console.log('Update 2'));
updater.schedule(() => console.log('Update 3'));

// Output:
// Processing 3 updates
// Update 1
// Update 2
// Update 3

Real-World Examples

Example 1: Debouncing with Event Loop

// Debounce using event loop knowledge
function debounce(func, delay) {
  let timeoutId;
  let lastArgs;
  let microtaskScheduled = false;
  
  return function(...args) {
    lastArgs = args;
    clearTimeout(timeoutId);
    
    if (!microtaskScheduled) {
      microtaskScheduled = true;
      
      Promise.resolve().then(() => {
        microtaskScheduled = false;
      });
    }
    
    timeoutId = setTimeout(() => {
      func.apply(this, lastArgs);
    }, delay);
  };
}

const debouncedLog = debounce(console.log, 100);
debouncedLog('Hello');
debouncedLog('World');
// Only 'World' is logged after 100ms

Example 2: Priority Queue

// Priority queue using microtasks and macrotasks
class PriorityQueue {
  constructor() {
    this.highPriority = [];
    this.normalPriority = [];
  }
  
  addHighPriority(task) {
    this.highPriority.push(task);
    
    Promise.resolve().then(() => {
      this.processHighPriority();
    });
  }
  
  addNormalPriority(task) {
    this.normalPriority.push(task);
    
    setTimeout(() => {
      this.processNormalPriority();
    }, 0);
  }
  
  processHighPriority() {
    while (this.highPriority.length > 0) {
      const task = this.highPriority.shift();
      task();
    }
  }
  
  processNormalPriority() {
    if (this.normalPriority.length > 0) {
      const task = this.normalPriority.shift();
      task();
    }
  }
}

const queue = new PriorityQueue();

queue.addNormalPriority(() => console.log('Normal 1'));
queue.addHighPriority(() => console.log('High 1'));
queue.addNormalPriority(() => console.log('Normal 2'));
queue.addHighPriority(() => console.log('High 2'));

// Output:
// High 1
// High 2
// Normal 1
// Normal 2

Example 3: Async Batch Processing

// Batch processing with event loop
class AsyncBatcher {
  constructor(batchSize = 10) {
    this.batchSize = batchSize;
    this.queue = [];
    this.processing = false;
  }
  
  add(item) {
    this.queue.push(item);
    
    if (this.queue.length >= this.batchSize) {
      this.processBatch();
    } else if (!this.processing) {
      this.processing = true;
      
      Promise.resolve().then(() => {
        if (this.queue.length > 0) {
          this.processBatch();
        }
        this.processing = false;
      });
    }
  }
  
  processBatch() {
    const batch = this.queue.splice(0, this.batchSize);
    console.log(`Processing batch of ${batch.length} items`);
    
    batch.forEach(item => {
      // Process item
    });
  }
}

const batcher = new AsyncBatcher(3);

batcher.add('item1');
batcher.add('item2');
batcher.add('item3');
// Processing batch of 3 items

batcher.add('item4');
// Scheduled for next microtask

Debugging Event Loop Issues

Identifying Blocking Code

// Detect blocking code
function measureEventLoopLag() {
  let lastTime = performance.now();
  
  setInterval(() => {
    const currentTime = performance.now();
    const lag = currentTime - lastTime - 1000; // Expected 1000ms
    
    if (lag > 50) {
      console.warn(`Event loop lag: ${lag}ms`);
    }
    
    lastTime = currentTime;
  }, 1000);
}

measureEventLoopLag();

Visualizing Task Execution

// Visualize task execution order
function visualizeEventLoop() {
  console.log('=== Event Loop Visualization ===');
  
  console.log('1. Synchronous code');
  
  setTimeout(() => {
    console.log('2. Macrotask (setTimeout)');
  }, 0);
  
  Promise.resolve().then(() => {
    console.log('3. Microtask (Promise)');
  });
  
  queueMicrotask(() => {
    console.log('4. Microtask (queueMicrotask)');
  });
  
  console.log('5. More synchronous code');
}

visualizeEventLoop();

// Output shows execution order clearly

Common Mistakes to Avoid

Mistake 1: Assuming setTimeout(0) is Immediate

// โŒ Wrong - Assuming setTimeout(0) runs immediately
setTimeout(() => {
  console.log('This is not immediate');
}, 0);

// โœ… Correct - Use Promise for immediate execution
Promise.resolve().then(() => {
  console.log('This is more immediate');
});

Mistake 2: Blocking the Event Loop

// โŒ Wrong - Blocking the event loop
function blockingLoop() {
  const start = performance.now();
  while (performance.now() - start < 1000) {
    // Busy wait - blocks everything
  }
}

// โœ… Correct - Use async operations
async function nonBlockingLoop() {
  for (let i = 0; i < 100; i++) {
    await new Promise(resolve => setTimeout(resolve, 10));
  }
}

Mistake 3: Microtask Starvation

// โŒ Wrong - Infinite microtasks starve macrotasks
function infiniteMicrotasks() {
  Promise.resolve().then(() => {
    console.log('Microtask');
    infiniteMicrotasks(); // Creates infinite loop
  });
}

// โœ… Correct - Limit microtask recursion
function limitedMicrotasks(count = 0) {
  if (count < 10) {
    Promise.resolve().then(() => {
      console.log('Microtask');
      limitedMicrotasks(count + 1);
    });
  }
}

Summary

The event loop is fundamental to JavaScript:

  • Event loop manages synchronous and asynchronous code
  • Microtasks (Promises) execute before macrotasks (setTimeout)
  • Call stack must be empty before processing tasks
  • Rendering happens between macrotasks
  • Understanding event loop improves performance
  • Avoid blocking the main thread
  • Use microtasks for high-priority operations
  • Use macrotasks for lower-priority operations
  • Monitor event loop lag in production

Next Steps

Continue your learning journey:

Comments