JavaScriptConcepts

JavaScript Event Loop - Complete Interview Guide

Table of Contents

  1. What is the Event Loop?
  2. Call Stack
  3. Web APIs
  4. Callback Queue (Task Queue)
  5. Microtask Queue
  6. Event Loop Process
  7. Code Examples with Outputs
  8. Common Interview Questions
  9. Advanced Concepts
  10. Best Practices

What is the Event Loop?

The Event Loop is the mechanism that allows JavaScript to perform non-blocking operations despite being single-threaded. It continuously monitors the call stack and queues, moving tasks from queues to the call stack when it’s empty.

Key Points:


Call Stack

The Call Stack is a LIFO (Last In, First Out) data structure that keeps track of function calls.

How it works:

  1. When a function is called, it’s pushed onto the stack
  2. When a function returns, it’s popped off the stack
  3. The stack must be empty for the event loop to process queued tasks

Example:

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

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

first();

Output:

First function
Second function

Call Stack Visualization:

Step 1: [first()]
Step 2: [first(), second()]
Step 3: [first()]  // second() popped
Step 4: []         // first() popped

Web APIs

Web APIs are browser-provided APIs that handle asynchronous operations like:

Example:

console.log("Start");

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

console.log("End");

Output:

Start
End
Timeout callback

Explanation: Even with 0ms delay, the setTimeout callback goes to Web APIs, then to the callback queue, and only executes after the call stack is empty.


Callback Queue (Task Queue)

The Callback Queue stores callbacks from Web APIs waiting to be executed. It follows FIFO (First In, First Out) principle.

Example:

console.log("1");

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

console.log("4");

Output:

1
4
2
3

Queue Order: Callbacks are processed in the order they were added to the queue.


Microtask Queue

The Microtask Queue has higher priority than the Callback Queue. It includes:

Priority Order:

  1. Call Stack (highest priority)
  2. Microtask Queue
  3. Callback Queue (lowest priority)

Example:

console.log("1");

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

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

console.log("4");

Output:

1
4
3
2

Explanation: Promise callback (microtask) executes before setTimeout callback (macrotask).


Event Loop Process

The Event Loop follows this continuous process:

  1. Execute all synchronous code in the call stack
  2. Check if call stack is empty
  3. Process all microtasks (Promise callbacks, etc.)
  4. Process one macrotask from callback queue
  5. Repeat the process

Visual Representation:

┌─────────────────┐
│   Call Stack    │ ← Executes functions
└─────────────────┘
         ↑
┌─────────────────┐
│  Microtask      │ ← Higher priority
│    Queue        │   (Promises, async/await)
└─────────────────┘
         ↑
┌─────────────────┐
│   Callback      │ ← Lower priority
│    Queue        │   (setTimeout, setInterval)
└─────────────────┘
         ↑
┌─────────────────┐
│   Web APIs      │ ← Handles async operations
└─────────────────┘

Code Examples with Outputs

Example 1: Basic Event Loop

console.log("Start");

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

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

console.log("End");

Output:

Start
End
Promise 1
Timeout 1

Example 2: Multiple Promises and Timeouts

console.log("1");

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

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

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

console.log("6");

Output:

1
6
3
4
2
5

Example 3: Nested Promises

console.log("A");

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

console.log("D");

Output:

A
D
B
C

Example 4: async/await with Event Loop

console.log("1");

async function asyncFunction() {
    console.log("2");
    await Promise.resolve();
    console.log("3");
}

asyncFunction();

console.log("4");

Output:

1
2
4
3

Example 5: Complex Event Loop

console.log("1");

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

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

console.log("6");

Output:

1
6
4
2
3
5

Example 6: Event Loop with setImmediate (Node.js)

// Node.js environment
console.log("1");

setImmediate(() => console.log("2"));

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

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

console.log("5");

Output:

1
5
4
3
2

Common Interview Questions

Q1: What’s the difference between microtasks and macrotasks?

Answer:

Q2: Why does this code output in this order?

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

Answer:

3
2
1

Q3: How does async/await work with the event loop?

Answer:

async function example() {
    console.log("A");
    await Promise.resolve();
    console.log("B"); // This becomes a microtask
}

example();
console.log("C");

Output:

A
C
B

Q4: What happens with nested setTimeout?

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

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

Output:

1
3
2

Advanced Concepts

1. Starvation of Macrotasks

function recursiveMicrotask() {
    Promise.resolve().then(() => {
        console.log("Microtask");
        recursiveMicrotask(); // This will starve macrotasks
    });
}

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

Warning: This creates an infinite loop of microtasks, preventing macrotasks from executing.

2. Event Loop Phases (Node.js)

  1. Timer Phase - setTimeout, setInterval
  2. Pending Callbacks - I/O callbacks
  3. Idle, Prepare - Internal use
  4. Poll - Fetch new I/O events
  5. Check - setImmediate callbacks
  6. Close Callbacks - Close events

3. requestAnimationFrame (Browser)

console.log("1");

requestAnimationFrame(() => console.log("2"));

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

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

console.log("5");

Output:

1
5
4
3
2

Best Practices

1. Avoid Blocking the Event Loop

// Bad - blocks event loop
function heavyTask() {
    let result = 0;
    for (let i = 0; i < 1000000000; i++) {
        result += i;
    }
    return result;
}

// Good - non-blocking
function heavyTaskAsync() {
    return new Promise((resolve) => {
        setTimeout(() => {
            let result = 0;
            for (let i = 0; i < 1000000000; i++) {
                result += i;
            }
            resolve(result);
        }, 0);
    });
}

2. Use Microtasks Wisely

// Prefer this for immediate execution after current task
Promise.resolve().then(() => {
    console.log("This runs before next macrotask");
});

// Over this for non-time-critical tasks
setTimeout(() => {
    console.log("This runs in next macrotask");
}, 0);

3. Understanding Promise Chaining

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

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

Output:

1
3
2

Summary

The JavaScript Event Loop is crucial for understanding asynchronous JavaScript. Remember:

  1. Call Stack executes synchronous code
  2. Microtask Queue has higher priority (Promises, async/await)
  3. Callback Queue has lower priority (setTimeout, setInterval)
  4. Event Loop continuously moves tasks from queues to call stack
  5. All microtasks are processed before the next macrotask

Understanding these concepts will help you write better asynchronous code and ace JavaScript interviews!


Practice Problems

Try to predict the output of these code snippets:

Problem 1:

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

Problem 2:

async function async1() {
    console.log("1");
    await async2();
    console.log("2");
}

async function async2() {
    console.log("3");
}

console.log("4");
async1();
console.log("5");

Solutions: