CrackFrontendCF
Resources
Practice
CrackFrontend

Your guide to mastering frontend interviews

Resources

LibraryPracticeDonate

Company

About UsContact

Legal

Privacy PolicyTerms of Service

Built with ❤️ by Tushar Khanna

CrackFrontendCF
Resources
Practice

⚡ JavaScript Event Loop: Complete Guide to Asynchronous Execution

Master JavaScript's Event Loop - the heart of asynchronous execution. Understand how the call stack, task queue, and microtask queue work together to handle async operations.

Interview Importance: 🔴 Critical — The Event Loop is asked in 85% of JavaScript interviews. Understanding it is essential for explaining how async code works, debugging timing issues, and architecting performant applications. -- 1️⃣ What is the Event Loop? The Event Loop is JavaScript's mechanism for handling asynchronous operations in a single-threaded environment. It continuously monitors the call stack and task queues, executing code in a specific order to create the illusion of concurrent execution. Visual Representation: Real-World Analogy: Restaurant Kitchen 🍳 Imagine a restaurant kitchen with one chef (single-threaded JavaScript): The chef can't cook multiple dishes simultaneously, but the manager ensures orders are handled efficiently by prioritizing and coordinating work. -- 2️⃣ Why Does This Matter? Common Use Cases Why Event Loop Matters Event loop delegates I/O to browser APIs, keeping UI responsive Event loop schedules rendering between tasks Events queue up without blocking execution Microtask queue ensures promises resolve before next task Understanding timing helps optimize performance Knowing execution order prevents bugs Performance Benefits -- 3️⃣ How It Works — Execution Order Basic Implementation Visualization 🔍 Dry Run: Event Loop Execution Code: Step-by-Step Execution: Key Insight: Microtasks ALWAYS execute before the next macrotask, even if the macrotask was queued first! -- 4️⃣ Understanding Key Concepts The Call Stack The call stack is where JavaScript tracks function execution. It follows LIFO (Last In, First Out). What happens if the call stack is never empty? -Infinite loop! The event loop can't process queued tasks. Microtask Queue (Job Queue) Microtasks have the highest priority and execute immediately after the current script finishes, before any macrotasks. Sources of Microtasks: , , callbacks (Node.js even higher priority than Promises!) Macrotask Queue (Task Queue) Macrotasks represent larger units of work. Only ONE macrotask executes per event loop iteration. Sources of Macrotasks: , (Node.js) I/O operations UI rendering User interaction events (click, scroll, etc.) Why This Line of Code is Critical What breaks if you remove the event loop? -JavaScript becomes synchronous! Every async operation would block execution until complete. Your web page would freeze during API calls, file reads, or any I/O. -- 5️⃣ Advanced Concepts — Event Loop Phases Complete Event Loop Cycle Production-Ready Example: Task Scheduler Microtask Starvation Why this matters: Infinite microtasks prevent rendering and user interaction! -- 6️⃣ Real-World Examples Example 1: setTimeout vs Promise Example 2: React useEffect Timing Example 3: Async/Await with Event Loop Key insight: pauses function execution and queues continuation as a microtask when the Promise resolves. Example 4: Event Loop in Node.js -- 7️⃣ Comparisons Microtasks vs Macrotasks Microtasks High execute immediately after current script ALL microtasks execute before next macrotask Promises, queueMicrotask, MutationObserver Run after synchronous code, before rendering State updates, quick tasks Can starve macrotasks if infinite Visual Comparison Browser vs Node.js Event Loop Browser HTML spec Simpler (microtasks -macrotask -render) Not available Not available Scheduled between tasks Interactive UIs -- 8️⃣ Common Interview Questions Q1: Why does setTimeout with 0ms delay not execute immediately? Answer: Because schedules a macrotask, which only executes after: 1. All synchronous code completes 2. Call stack is empty 3. All microtasks are processed Even with 0ms delay, it must wait for the event loop to reach the macrotask queue. Q2: What's the output of this code? Answer: Q3: Can you explain this async/await behavior? Answer: runs synchronously pauses the function and schedules the continuation as a microtask Execution returns to caller runs synchronously Call stack empties, microtask runs executes Q4: What happens if a microtask creates another microtask infinitely? Answer: This is called microtask starvation. The event loop is stuck processing microtasks forever. Q5: How do Promises queue in the event loop? Answer: Promise executor runs synchronously Each queues a microtask Microtasks execute after synchronous code Chained callbacks execute in order Q6: Explain the difference in timing between these two: Answer: Version B executes first because: -Macrotask queue -Microtask queue Event loop prioritizes microtasks over macrotasks -- 9️⃣ Common Pitfalls Pitfall 1: Assuming setTimeout is Accurate ❌ BAD: Expecting precise timing ✅ GOOD: Understanding setTimeout is a minimum delay Why it fails: setTimeout can't interrupt the call stack. If synchronous code runs long, the timeout waits. Pitfall 2: Creating Infinite Microtasks ❌ BAD: Recursive microtask without exit condition ✅ GOOD: Use macrotasks for recursive operations Why it fails: Microtasks must all complete before macrotasks run. Infinite microtasks = frozen UI. Pitfall 3: Race Conditions with Mixed Async ❌ BAD: Mixing setTimeout and Promises without understanding order ✅ GOOD: Explicitly control execution order Why it fails: Promises (microtasks) execute before setTimeout (macrotasks), regardless of code order. Pitfall 4: Not Understanding async/await Execution ❌ BAD: Assuming async functions run completely asynchronously ✅ GOOD: Understanding async functions run synchronously until first await Why it fails: functions run synchronously until they hit . Only the code after is deferred as a microtask. -- 🔟 Time & Space Complexity Event Loop Operations Time Complexity O(1) O(1) O(1) O(1) O(m) O(m × n) O(m × n t) Space Complexity Space Complexity O(d) O(m) O(t) O(d m t) Note: The event loop itself is a conceptual model, not a data structure with complexity. The complexities above refer to the underlying queues and stack. -- 📝 Summary Quick Reference Table Key Points Coordinates execution between call stack and task queues Call Stack High priority (Promises, queueMicrotask), ALL execute before next macrotask Macrotasks Sync code -All microtasks -Render -One macrotask -Repeat async/await Infinite microtasks block macrotasks and rendering | 5 Key Takeaways 1. JavaScript is single-threaded — The event loop creates the illusion of concurrency by coordinating between synchronous execution and async callbacks 2. Microtasks have priority — ALL microtasks execute before ANY macrotask. This is why Promises resolve before setTimeout callbacks 3. setTimeout(fn, 0) is NOT immediate — It schedules a macrotask that runs after synchronous code and all microtasks complete 4. async/await uses microtasks — Code after runs as a microtask when the Promise resolves, not immediately 5. Understanding execution order prevents bugs — Knowing when callbacks execute helps debug timing issues, race conditions, and unexpected behavior Best Practices ✅ Use Promises/async-await for async operations (microtasks) ✅ Use setTimeout for deferred work (macrotasks) ✅ Avoid long-running synchronous code that blocks the event loop ✅ Break heavy computations into chunks with setTimeout or requestIdleCallback ✅ Remember: Microtasks -Rendering -Macrotask (one cycle) ❌ Don't create infinite microtasks (causes UI freeze) ❌ Don't assume setTimeout timing is precise ❌ Don't mix async primitives without understanding execution order -- 📚 Further Reading MDN: Event Loop Official documentation with detailed explanations Jake Archibald: In The Loop Excellent conference talk with visualizations HTML Living Standard: Event Loops Official specification Node.js Event Loop Documentation Node.js-specific implementation details -- 🔗 Related Resources Closures () Understanding function scope and callbacks Promises () Async patterns and microtask behavior Debounce () and Throttle () Practical event loop applications -- <!-quiz-start --Q1: What is the output of this code? [ ] A, B, C, D [ ] A, D, B, C [x] A, D, C, B [ ] A, C, D, B Q2: Which queue has higher priority in the event loop? [ ] Macrotask queue [x] Microtask queue [ ] Both have equal priority [ ] It depends on the browser Q3: What happens when setTimeout is called with a delay of 0ms? [ ] The callback executes immediately [ ] The callback executes before synchronous code [x] The callback is added to the macrotask queue and executes after all synchronous code and microtasks [ ] The delay is ignored and treated as 1ms <!-quiz-end --
JavaScriptCore Concepts
🛑 AbortController: Canceling Async Operations in JavaScript
medium
🔒 Closures in JavaScript — The Complete Guide
hard
📦 Understanding ES6 Modules in JavaScript
medium
⚡ JavaScript Event Loop: Complete Guide to Asynchronous Execution
hard
🧭 Arrow Functions vs Function Declarations in JavaScript
easy
🗑️ Garbage Collection in JavaScript — Memory Management & Leak Prevention
hard
🏗️ Constructor Functions in JavaScript
medium
👁️ MutationObserver: Watching DOM Changes in JavaScript
medium
🔍 Understanding `of` in JavaScript – `for...of` Loop Deep Dive
hard
🔗 Prototype and Prototype Inheritance in JavaScript
medium
🕵️ What Are Proxies in JavaScript? (With Practical Use Cases)
medium
🎯 Scope in JavaScript — The Complete Guide
hard
🔄 Script Loading: async vs defer vs Both
hard
📤 JavaScript Spread Operator (...) Explained
easy
🎯 The JavaScript `this` Keyword: Complete Guide to Context Binding
medium
4 of 15
LibraryJavaScriptCore Concepts4 of 61

⚡ JavaScript Event Loop: Complete Guide to Asynchronous Execution

jsgeneral-conceptshard

Interview Importance: 🔴 Critical — The Event Loop is asked in 85% of JavaScript interviews. Understanding it is essential for explaining how async code works, debugging timing issues, and architecting performant applications.


1️⃣ What is the Event Loop?

The Event Loop is JavaScript's mechanism for handling asynchronous operations in a single-threaded environment. It continuously monitors the call stack and task queues, executing code in a specific order to create the illusion of concurrent execution.

Visual Representation:

+---------------------------------------------------------------+
|                    JAVASCRIPT RUNTIME                         |
|                                                               |
|  +---------------------+    +---------------------------+   |
|  |    Call Stack       |    |   Web APIs / Node APIs     |   |
|  |                     |    |                            |   |
|  |  main()             |    |  • setTimeout()            |   |
|  |  v foo()            |    |  • fetch()                 |   |
|  |  v bar()            |    |  • DOM events              |   |
|  |                     |    |  • Promises                |   |
|  +---------------------+    +---------------------------+   |
|            ^                            v                     |
|            |                            |                     |
|            |                   Callbacks Ready                |
|            |                            v                     |
|  +---------+----------------------------+--------------+    |
|  |              EVENT LOOP (Coordinator)                |    |
|  |                                                      |    |
|  |  Checks: Is call stack empty?                       |    |
|  |  Yes? -> Move task from queue to call stack          |    |
|  +------------------------------------------------------+    |
|            ^                                                  |
|  +---------+-------------+    +--------------------------+  |
|  |  Microtask Queue      |    |   Macrotask Queue        |  |
|  |  (Higher Priority)    |    |   (Lower Priority)       |  |
|  |                       |    |                          |  |
|  |  • Promise callbacks  |    |  • setTimeout callbacks  |  |
|  |  • queueMicrotask()   |    |  • setInterval callbacks |  |
|  |  • MutationObserver   |    |  • I/O operations        |  |
|  |  • process.nextTick() |    |  • UI rendering          |  |
|  +-----------------------+    +--------------------------+  |
+---------------------------------------------------------------+

Real-World Analogy: Restaurant Kitchen 🍳

Imagine a restaurant kitchen with one chef (single-threaded JavaScript):

+--------------------------------------------------------------+
|  🧑‍🍳 CHEF (Call Stack)                                         |
|  Currently cooking dishes one at a time                      |
|                                                              |
|  📋 ORDER SLIPS (Task Queues)                                |
|  +--------------------+    +----------------------------+   |
|  |  VIP Orders        |    |  Regular Orders            |   |
|  |  (Microtasks)      |    |  (Macrotasks)              |   |
|  |  • Appetizers      |    |  • Main courses            |   |
|  |  • Quick fixes     |    |  • Desserts                |   |
|  +--------------------+    +----------------------------+   |
|                                                              |
|  🔄 MANAGER (Event Loop)                                     |
|  Watches chef and decides what to cook next:                 |
|  1. Let chef finish current dish                            |
|  2. Check VIP orders first (microtasks)                     |
|  3. Then regular orders (macrotasks)                        |
|  4. Repeat                                                  |
+--------------------------------------------------------------+

The chef can't cook multiple dishes simultaneously, but the manager ensures orders are handled efficiently by prioritizing and coordinating work.


2️⃣ Why Does This Matter?

Common Use Cases

ProblemWhy Event Loop MattersReal Example
API calls blocking UIEvent loop delegates I/O to browser APIs, keeping UI responsivefetch() doesn't freeze the page
Animation frame timingEvent loop schedules rendering between tasksrequestAnimationFrame()
User input responsivenessEvents queue up without blocking executionClick handlers work during loading
Promise resolution orderMicrotask queue ensures promises resolve before next task.then() runs before setTimeout()
Debouncing/throttlingUnderstanding timing helps optimize performanceSearch autocomplete
Race conditionsKnowing execution order prevents bugsMultiple async state updates

Performance Benefits

Without Event Loop (Blocking):
User clicks button -> Wait 5s for API -> UI frozen -> User frustrated

With Event Loop (Non-blocking):
User clicks button -> API request sent to Web API -> UI stays responsive
-> User can scroll, type -> API response ready -> Callback executes
-> UI updates -> Happy user!

3️⃣ How It Works — Execution Order

Basic Implementation Visualization

// Example demonstrating execution order
console.log('1: Synchronous start');

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

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

console.log('4: Synchronous end');

// OUTPUT:
// 1: Synchronous start
// 4: Synchronous end
// 3: Microtask (Promise)
// 2: Macrotask (setTimeout)

🔍 Dry Run: Event Loop Execution

Code:

console.log('Start');

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

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

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

console.log('End');

Step-by-Step Execution:

INITIAL STATE:
---------------------------------------------------------
Call Stack:        [global execution context]
Microtask Queue:   []
Macrotask Queue:   []
Output:            []


STEP 1: console.log('Start')
---------------------------------------------------------
Call Stack:        [global, console.log]
Action:            Execute synchronous code
Output:            ['Start']


STEP 2: setTimeout(..., 0)
---------------------------------------------------------
Call Stack:        [global, setTimeout]
Action:            Register callback with Web API, returns immediately
Macrotask Queue:   [() => console.log('Timeout 1')]
Call Stack:        [global]  <-- setTimeout popped off


STEP 3: Promise.resolve().then(...)
---------------------------------------------------------
Call Stack:        [global, Promise.resolve().then]
Action:            Promise already resolved, queue microtask
Microtask Queue:   [() => console.log('Promise 1')]
Call Stack:        [global]  <-- Promise.then popped off


STEP 4: Second setTimeout
---------------------------------------------------------
Call Stack:        [global, setTimeout]
Action:            Register second callback
Macrotask Queue:   [
                     () => console.log('Timeout 1'),
                     () => console.log('Timeout 2')
                   ]
Call Stack:        [global]


STEP 5: console.log('End')
---------------------------------------------------------
Call Stack:        [global, console.log]
Action:            Execute synchronous code
Output:            ['Start', 'End']
Call Stack:        [global]


STEP 6: Global execution context completes
---------------------------------------------------------
Call Stack:        []  <-- NOW EMPTY!
Event Loop:        Checks call stack, finds it empty
                   Checks Microtask Queue FIRST


STEP 7: Execute all microtasks
---------------------------------------------------------
Microtask Queue:   [() => console.log('Promise 1')]
Call Stack:        [() => console.log('Promise 1')]
Action:            Execute microtask
Output:            ['Start', 'End', 'Promise 1']
Call Stack:        []

Action:            First .then() creates second .then()
Microtask Queue:   [() => console.log('Promise 2')]
Call Stack:        [() => console.log('Promise 2')]
Output:            ['Start', 'End', 'Promise 1', 'Promise 2']
Call Stack:        []


STEP 8: Microtask queue empty, check Macrotask queue
---------------------------------------------------------
Macrotask Queue:   [
                     () => console.log('Timeout 1'),
                     () => console.log('Timeout 2')
                   ]
Call Stack:        [() => console.log('Timeout 1')]
Action:            Execute first macrotask
Output:            ['Start', 'End', 'Promise 1', 'Promise 2', 'Timeout 1']
Call Stack:        []


STEP 9: Check microtasks again (none), execute next macrotask
---------------------------------------------------------
Call Stack:        [() => console.log('Timeout 2')]
Output:            ['Start', 'End', 'Promise 1', 'Promise 2', 
                    'Timeout 1', 'Timeout 2']
Call Stack:        []


FINAL STATE:
---------------------------------------------------------
All queues empty, event loop continues monitoring...

Key Insight: Microtasks ALWAYS execute before the next macrotask, even if the macrotask was queued first!


4️⃣ Understanding Key Concepts

The Call Stack

The call stack is where JavaScript tracks function execution. It follows LIFO (Last In, First Out).

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

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

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

first();

// Call Stack Visualization:
// Step 1: [first]
// Step 2: [first, second]
// Step 3: [first, second, third]
// Step 4: [first, second]         <-- third completes
// Step 5: [first]                 <-- second completes
// Step 6: []                      <-- first completes

What happens if the call stack is never empty? -> Infinite loop! The event loop can't process queued tasks.

// ❌ BAD: Blocks event loop
while (true) {
  // Call stack never empties
}
// Any setTimeout or Promise callbacks will NEVER execute!

Microtask Queue (Job Queue)

Microtasks have the highest priority and execute immediately after the current script finishes, before any macrotasks.

Sources of Microtasks:

  • Promise.then(), Promise.catch(), Promise.finally()
  • queueMicrotask()
  • MutationObserver callbacks
  • process.nextTick() (Node.js - even higher priority than Promises!)
// Microtasks execute between synchronous code and macrotasks
console.log('Sync 1');

queueMicrotask(() => console.log('Microtask'));

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

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

console.log('Sync 2');

// Output:
// Sync 1
// Sync 2
// Microtask     <-- Both microtasks run together
// Promise       <-- Before any macrotask
// Timeout       <-- Macrotask runs last

Macrotask Queue (Task Queue)

Macrotasks represent larger units of work. Only ONE macrotask executes per event loop iteration.

Sources of Macrotasks:

  • setTimeout(), setInterval()
  • setImmediate() (Node.js)
  • I/O operations
  • UI rendering
  • User interaction events (click, scroll, etc.)
// Each setTimeout creates a separate macrotask
setTimeout(() => console.log('Timeout 1'), 0);
setTimeout(() => console.log('Timeout 2'), 0);
setTimeout(() => console.log('Timeout 3'), 0);

// Event loop will execute them one per iteration:
// Iteration 1: Timeout 1
// Iteration 2: Timeout 2
// Iteration 3: Timeout 3

Why This Line of Code is Critical

// This line is what makes JavaScript non-blocking:
setTimeout(() => {
  // This callback is NOT executed immediately
  // It's scheduled as a macrotask for later
}, 0);

// JavaScript IMMEDIATELY continues here
console.log('This runs first!');

What breaks if you remove the event loop? -> JavaScript becomes synchronous! Every async operation would block execution until complete. Your web page would freeze during API calls, file reads, or any I/O.


5️⃣ Advanced Concepts — Event Loop Phases

Complete Event Loop Cycle

+---------------------------------------------------------+
|                   EVENT LOOP CYCLE                      |
+---------------------------------------------------------+
         |
         ▼
    +---------+
    | Start   |
    +----+----+
         |
         ▼
    +------------------------+
    | 1. Execute Script      |
    |    (Synchronous Code)  |
    +--------+---------------+
         |
         ▼
    +------------------------+
    | 2. Process All         |
    |    Microtasks          |---+ Keep processing until
    +--------+---------------+   | microtask queue is empty
         |   ▲                   |
         |   +-------------------+
         ▼
    +------------------------+
    | 3. Render UI           |
    |    (if needed)         |
    +--------+---------------+
         |
         ▼
    +------------------------+
    | 4. Execute ONE         |
    |    Macrotask           |
    +--------+---------------+
         |
         ▼
    +------------------------+
    | 5. Process All         |---+ After each macrotask,
    |    Microtasks Again    |   | process all microtasks
    +--------+---------------+   |
         |   ▲                   |
         |   +-------------------+
         |
         +----------+
                    |
                    ▼
              +----------+
              | Repeat   |
              +----------+

Production-Ready Example: Task Scheduler

/**
 * Task Scheduler demonstrating event loop behavior
 * Shows microtask vs macrotask execution order
 */
class TaskScheduler {
  constructor() {
    this.executionLog = [];
  }

  // Schedule a microtask (high priority)
  scheduleMicrotask(name, task) {
    this.executionLog.push(`Scheduled microtask: ${name}`);
    queueMicrotask(() => {
      this.executionLog.push(`Executing microtask: ${name}`);
      task();
    });
  }

  // Schedule a macrotask (lower priority)
  scheduleMacrotask(name, task, delay = 0) {
    this.executionLog.push(`Scheduled macrotask: ${name}`);
    setTimeout(() => {
      this.executionLog.push(`Executing macrotask: ${name}`);
      task();
    }, delay);
  }

  // Schedule via Promise (microtask)
  schedulePromise(name, task) {
    this.executionLog.push(`Scheduled promise: ${name}`);
    Promise.resolve().then(() => {
      this.executionLog.push(`Executing promise: ${name}`);
      task();
    });
  }

  // Get execution log
  getLog() {
    return this.executionLog;
  }

  // Clear log
  clearLog() {
    this.executionLog = [];
  }
}

// Usage example
const scheduler = new TaskScheduler();

console.log('=== Scheduling Phase ===');

scheduler.scheduleMacrotask('Fetch Data', () => {
  console.log('Fetching data from API...');
});

scheduler.scheduleMicrotask('Validate Input', () => {
  console.log('Validating user input...');
});

scheduler.schedulePromise('Process Response', () => {
  console.log('Processing API response...');
});

scheduler.scheduleMacrotask('Update UI', () => {
  console.log('Updating UI...');
});

console.log('=== Execution Phase ===');

// After event loop completes, check log
setTimeout(() => {
  console.log('\n=== Execution Order ===');
  scheduler.getLog().forEach(log => console.log(log));
}, 100);

Microtask Starvation

// ⚠️ WARNING: This can starve the macrotask queue!
function recursiveMicrotask() {
  queueMicrotask(() => {
    console.log('Microtask executing');
    recursiveMicrotask(); // Creates another microtask
  });
}

setTimeout(() => {
  console.log('This macrotask may NEVER run!');
}, 0);

// Don't do this! Microtasks will run forever, blocking macrotasks
// recursiveMicrotask();

Why this matters: Infinite microtasks prevent rendering and user interaction!


6️⃣ Real-World Examples

Example 1: setTimeout vs Promise

// Common interview question
console.log('Script start');

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

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

console.log('Script end');

// Output:
// Script start
// Script end
// Promise 1              <-- Microtasks run first
// Promise 2              <-- All microtasks complete
// setTimeout 1           <-- First macrotask
// Promise inside setTimeout  <-- Microtask from first macrotask
// setTimeout inside Promise  <-- Second macrotask

Example 2: React useEffect Timing

import { useEffect, useState } from 'react';

function DataFetcher() {
  const [data, setData] = useState(null);

  useEffect(() => {
    console.log('1: useEffect starts');
    
    // Macrotask
    setTimeout(() => {
      console.log('4: setTimeout callback');
    }, 0);

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

    // Synchronous
    console.log('2: useEffect ends');

    // Cleanup function
    return () => {
      console.log('5: Cleanup (on unmount)');
    };
  }, []);

  return <div>Check console for event loop demo</div>;
}

// Output on mount:
// 1: useEffect starts
// 2: useEffect ends
// 3: Promise callback
// 4: setTimeout callback
//
// Output on unmount:
// 5: Cleanup (on unmount)

Example 3: Async/Await with Event Loop

async function fetchUserData(userId) {
  console.log('1: Function start');

  // await creates a microtask
  const user = await fetch(`/api/users/${userId}`);
  console.log('3: After first await');

  const posts = await fetch(`/api/users/${userId}/posts`);
  console.log('5: After second await');

  return { user, posts };
}

console.log('0: Before function call');
fetchUserData(123);
console.log('2: After function call');

// Execution order:
// 0: Before function call
// 1: Function start
// 2: After function call
// (fetch completes...)
// 3: After first await
// (second fetch completes...)
// 5: After second await

Key insight: await pauses function execution and queues continuation as a microtask when the Promise resolves.

Example 4: Event Loop in Node.js

// Node.js has additional event loop phases
const fs = require('fs');

console.log('1: Synchronous');

// I/O operation (Poll phase)
fs.readFile('./file.txt', () => {
  console.log('5: File read complete');
  
  // setImmediate runs in Check phase
  setImmediate(() => console.log('6: setImmediate'));
  
  // setTimeout runs in Timer phase
  setTimeout(() => console.log('7: setTimeout'), 0);
  
  // nextTick has highest priority
  process.nextTick(() => console.log('4: nextTick inside I/O'));
});

// Microtask
Promise.resolve().then(() => console.log('3: Promise'));

// nextTick (higher priority than Promise!)
process.nextTick(() => console.log('2: nextTick'));

// Output (Node.js):
// 1: Synchronous
// 2: nextTick
// 3: Promise
// 5: File read complete
// 4: nextTick inside I/O
// 6: setImmediate
// 7: setTimeout

7️⃣ Comparisons

Microtasks vs Macrotasks

FeatureMicrotasksMacrotasks
PriorityHigh - execute immediately after current scriptLow - execute one per event loop cycle
ExecutionALL microtasks execute before next macrotaskONE macrotask per cycle
SourcesPromises, queueMicrotask, MutationObserversetTimeout, setInterval, I/O, UI events
TimingRun after synchronous code, before renderingRun after microtasks, may trigger rendering
Use CaseState updates, quick tasksDeferred work, animations, user events
RiskCan starve macrotasks if infiniteCan delay if too many queued

Visual Comparison

EXECUTION ORDER:

Synchronous Code
    v
Microtask 1
Microtask 2
Microtask 3...
(ALL microtasks)
    v
Render (if needed)
    v
Macrotask 1
    v
Microtask 4
Microtask 5...
(ALL microtasks)
    v
Render (if needed)
    v
Macrotask 2
    v
...repeat

Browser vs Node.js Event Loop

AspectBrowserNode.js
ImplementationHTML speclibuv library
PhasesSimpler (microtasks -> macrotask -> render)Complex (6 phases: timers, I/O, idle, poll, check, close)
nextTickNot availableHighest priority (even above microtasks)
setImmediateNot availableRuns in Check phase
RenderingScheduled between tasksNo rendering
Use CaseInteractive UIsServer operations, file I/O

8️⃣ Common Interview Questions

Q1: Why does setTimeout with 0ms delay not execute immediately?

Answer:

console.log('Start');
setTimeout(() => console.log('Timeout'), 0);
console.log('End');

// Output:
// Start
// End
// Timeout <-- Delayed!

Because setTimeout schedules a macrotask, which only executes after:

  1. All synchronous code completes
  2. Call stack is empty
  3. All microtasks are processed

Even with 0ms delay, it must wait for the event loop to reach the macrotask queue.

Q2: What's the output of this code?

Promise.resolve().then(() => console.log('A'));
queueMicrotask(() => console.log('B'));
setTimeout(() => console.log('C'), 0);
Promise.resolve().then(() => console.log('D'));
console.log('E');

Answer:

E  <-- Synchronous first
A  <-- Microtasks execute in order (Promise)
B  <-- queueMicrotask
D  <-- Promise
C  <-- Macrotask last

Q3: Can you explain this async/await behavior?

async function test() {
  console.log('1');
  await Promise.resolve();
  console.log('2');
}

test();
console.log('3');

// Output: 1, 3, 2 — Why?

Answer:

  • console.log('1') runs synchronously
  • await pauses the function and schedules the continuation as a microtask
  • Execution returns to caller
  • console.log('3') runs synchronously
  • Call stack empties, microtask runs
  • console.log('2') executes

Q4: What happens if a microtask creates another microtask infinitely?

Answer:

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

infiniteMicrotask();

// Result: Page freezes! Microtasks never end, so:
// - Macrotasks never execute
// - Rendering never happens
// - User interactions are blocked

This is called microtask starvation. The event loop is stuck processing microtasks forever.

Q5: How do Promises queue in the event loop?

Answer:

new Promise((resolve) => {
  console.log('1: Promise executor (synchronous)');
  resolve();
}).then(() => {
  console.log('2: First .then (microtask)');
  return 'value';
}).then((val) => {
  console.log('3: Second .then (microtask)');
});

console.log('4: Synchronous code');

// Output: 1, 4, 2, 3
  • Promise executor runs synchronously
  • Each .then() queues a microtask
  • Microtasks execute after synchronous code
  • Chained .then() callbacks execute in order

Q6: Explain the difference in timing between these two:

// Version A
setTimeout(() => console.log('A'), 0);

// Version B
Promise.resolve().then(() => console.log('B'));

Answer:

Version B executes first because:

  • setTimeout -> Macrotask queue
  • Promise.then -> Microtask queue
  • Event loop prioritizes microtasks over macrotasks
Event Loop Order:
1. Execute synchronous code
2. Process ALL microtasks -> 'B' prints
3. Process ONE macrotask -> 'A' prints

9️⃣ Common Pitfalls

Pitfall 1: Assuming setTimeout is Accurate

❌ BAD: Expecting precise timing

const start = Date.now();

setTimeout(() => {
  const elapsed = Date.now() - start;
  console.log(`Expected 100ms, got ${elapsed}ms`);
  // Might output: "Expected 100ms, got 105ms"
}, 100);

// Heavy computation blocks the event loop
for (let i = 0; i < 1000000000; i++) {
  // Blocking work
}

✅ GOOD: Understanding setTimeout is a minimum delay

// setTimeout(fn, delay) means:
// "Execute fn AT LEAST 'delay' ms from now"

// Better approach for precise timing:
let lastTime = performance.now();

function animate(currentTime) {
  const delta = currentTime - lastTime;
  console.log(`Actual time between frames: ${delta}ms`);
  
  lastTime = currentTime;
  requestAnimationFrame(animate);
}

requestAnimationFrame(animate);

Why it fails: setTimeout can't interrupt the call stack. If synchronous code runs long, the timeout waits.

Pitfall 2: Creating Infinite Microtasks

❌ BAD: Recursive microtask without exit condition

function processQueue() {
  queueMicrotask(() => {
    console.log('Processing...');
    processQueue(); // <-- Infinite recursion!
  });
}

processQueue();

// Result: Browser tab freezes, UI unresponsive
// Macrotasks (including rendering) never execute

✅ GOOD: Use macrotasks for recursive operations

function processQueue(items, index = 0) {
  if (index >= items.length) return; // Exit condition
  
  setTimeout(() => {
    console.log(`Processing item ${index}`);
    processQueue(items, index + 1);
  }, 0);
  
  // Allows other macrotasks and rendering between iterations
}

processQueue([1, 2, 3, 4, 5]);

Why it fails: Microtasks must all complete before macrotasks run. Infinite microtasks = frozen UI.

Pitfall 3: Race Conditions with Mixed Async

❌ BAD: Mixing setTimeout and Promises without understanding order

let counter = 0;

setTimeout(() => {
  counter++;
  console.log(`Timeout: ${counter}`); // Prints: "Timeout: 2"
}, 0);

Promise.resolve().then(() => {
  counter++;
  console.log(`Promise: ${counter}`); // Prints: "Promise: 1"
});

// Assuming timeout runs first? Wrong!

✅ GOOD: Explicitly control execution order

let counter = 0;

async function updateCounterSequentially() {
  // Force sequential execution
  await Promise.resolve();
  counter++;
  console.log(`First: ${counter}`);
  
  await new Promise(resolve => setTimeout(resolve, 0));
  counter++;
  console.log(`Second: ${counter}`);
}

updateCounterSequentially();

Why it fails: Promises (microtasks) execute before setTimeout (macrotasks), regardless of code order.

Pitfall 4: Not Understanding async/await Execution

❌ BAD: Assuming async functions run completely asynchronously

async function fetchData() {
  console.log('1: Inside async function');
  const data = await fetch('/api/data');
  console.log('3: After await');
  return data;
}

console.log('0: Before call');
fetchData();
console.log('2: After call');

// Expecting: 0, 2, 1, 3?
// Actually:  0, 1, 2, 3

✅ GOOD: Understanding async functions run synchronously until first await

async function fetchData() {
  // This part runs SYNCHRONOUSLY
  console.log('1: Inside async function (sync)');
  console.log('Starting fetch...');
  
  // Here execution pauses and returns to caller
  const data = await fetch('/api/data');
  
  // This part runs as a microtask after Promise resolves
  console.log('3: After await (async)');
  return data;
}

console.log('0: Before call');
fetchData(); // Returns a Promise immediately
console.log('2: After call (continues synchronously)');

Why it fails: async functions run synchronously until they hit await. Only the code after await is deferred as a microtask.


🔟 Time & Space Complexity

Event Loop Operations

OperationTime ComplexityExplanation
Push to microtask queueO(1)Constant time insertion
Push to macrotask queueO(1)Constant time insertion
Pop from queueO(1)Constant time removal
Check if call stack emptyO(1)Single comparison
Execute one microtaskO(m)Depends on microtask code
Execute all microtasksO(m × n)m = code complexity, n = number of microtasks
One event loop cycleO(m × n + t)n microtasks + 1 macrotask (t)

Space Complexity

StructureSpace ComplexityNotes
Call stackO(d)d = maximum call depth
Microtask queueO(m)m = number of queued microtasks
Macrotask queueO(t)t = number of queued macrotasks
OverallO(d + m + t)Sum of all structures

Note: The event loop itself is a conceptual model, not a data structure with complexity. The complexities above refer to the underlying queues and stack.


📝 Summary

Quick Reference Table

ConceptKey Points
Event LoopCoordinates execution between call stack and task queues
Call StackLIFO execution of functions, must empty before tasks execute
MicrotasksHigh priority (Promises, queueMicrotask), ALL execute before next macrotask
MacrotasksLower priority (setTimeout, I/O), ONE executes per event loop cycle
Execution OrderSync code -> All microtasks -> Render -> One macrotask -> Repeat
async/awaitRuns sync until await, then queues continuation as microtask
StarvationInfinite microtasks block macrotasks and rendering

5 Key Takeaways

  1. JavaScript is single-threaded — The event loop creates the illusion of concurrency by coordinating between synchronous execution and async callbacks

  2. Microtasks have priority — ALL microtasks execute before ANY macrotask. This is why Promises resolve before setTimeout callbacks

  3. setTimeout(fn, 0) is NOT immediate — It schedules a macrotask that runs after synchronous code and all microtasks complete

  4. async/await uses microtasks — Code after await runs as a microtask when the Promise resolves, not immediately

  5. Understanding execution order prevents bugs — Knowing when callbacks execute helps debug timing issues, race conditions, and unexpected behavior

Best Practices

  • ✅ Use Promises/async-await for async operations (microtasks)
  • ✅ Use setTimeout for deferred work (macrotasks)
  • ✅ Avoid long-running synchronous code that blocks the event loop
  • ✅ Break heavy computations into chunks with setTimeout or requestIdleCallback
  • ✅ Remember: Microtasks -> Rendering -> Macrotask (one cycle)
  • ❌ Don't create infinite microtasks (causes UI freeze)
  • ❌ Don't assume setTimeout timing is precise
  • ❌ Don't mix async primitives without understanding execution order

📚 Further Reading

  • MDN: Event Loop - Official documentation with detailed explanations
  • Jake Archibald: In The Loop - Excellent conference talk with visualizations
  • HTML Living Standard: Event Loops - Official specification
  • Node.js Event Loop Documentation - Node.js-specific implementation details

🔗 Related Resources

  • Closures (js/general-concepts/closures.md) - Understanding function scope and callbacks
  • Promises (js/promises/) - Async patterns and microtask behavior
  • Debounce (js/utils/debounce.md) and Throttle (js/utils/throttle.md) - Practical event loop applications

Quick Quiz

Test your understanding with 3 quick questions

Q1What is the output of this code?
Q2Which queue has higher priority in the event loop?
Q3What happens when setTimeout is called with a delay of 0ms?

Continue Reading

CrackFrontend

Your guide to mastering frontend interviews

Resources

LibraryPracticeDonate

Company

About UsContact

Legal

Privacy PolicyTerms of Service

Built with ❤️ by Tushar Khanna