15+ JavaScript Event Loop Interview Questions 2025: Microtasks, Macrotasks & Async

·9 min read
javascriptinterview-questionsevent-loopasyncfrontend

JavaScript is single-threaded, yet it handles thousands of concurrent operations without blocking. The event loop—a mechanism so fundamental that understanding it separates developers who write async code from those who truly control it. When interviewers ask you to predict the output of setTimeout mixed with Promises, they're testing this exact understanding.

Table of Contents

  1. Event Loop Fundamentals Questions
  2. Microtasks vs Macrotasks Questions
  3. Code Output Prediction Questions
  4. Async/Await Questions
  5. Node.js Event Loop Questions
  6. Practical Application Questions
  7. Quick Reference

Event Loop Fundamentals Questions

These questions test your understanding of the event loop's core mechanism.

What is the JavaScript Event Loop?

The Event Loop is what allows JavaScript to be non-blocking despite being single-threaded. It continuously checks if the call stack is empty, and if so, takes the first task from the queue and pushes it onto the stack. This is how JavaScript handles async operations—they're offloaded, and their callbacks are queued for later execution.

JavaScript has a single call stack where code executes. When we call an async function like setTimeout, it's handed off to the browser's Web APIs. Once the timer completes, the callback is placed in the task queue. The Event Loop's job is simple: check if the call stack is empty, then take the first task from the queue and push it onto the stack.

What are the main components of the Event Loop?

The Event Loop consists of several interconnected parts that work together to manage asynchronous execution:

ComponentPurpose
Call StackWhere function execution contexts are managed (LIFO)
Web APIsBrowser-provided APIs that handle async operations (setTimeout, fetch, DOM events)
Macrotask QueueHolds callbacks from setTimeout, setInterval, I/O operations
Microtask QueueHolds Promise callbacks, queueMicrotask, MutationObserver
Event LoopThe coordinator that moves tasks to the call stack when it's empty

The execution order is: Sync code → All Microtasks → One Macrotask → Repeat.

What happens to the event loop during a long-running script?

The call stack must be empty for the event loop to process any tasks. A long-running synchronous script blocks everything—no events fire, no callbacks execute, the UI freezes.

This is why we never put heavy computation in the main thread without breaking it up. If you have a loop processing 10,000 items synchronously, the browser cannot respond to user clicks, animations freeze, and the page becomes unresponsive.


Microtasks vs Macrotasks Questions

These questions test your understanding of task queue priorities.

What is the difference between microtasks and macrotasks?

Microtasks have higher priority and are processed immediately after the current script completes, before any macrotasks. The entire microtask queue is emptied before the next macrotask runs.

Microtasks:

  • Promise callbacks (.then(), .catch(), .finally())
  • queueMicrotask()
  • MutationObserver

Macrotasks:

  • setTimeout / setInterval
  • I/O operations
  • UI rendering
  • Event handlers (click, scroll, etc.)

This priority difference is why Promise.then() always executes before setTimeout(..., 0).

Why does Promise execute before setTimeout even with 0 delay?

Promises use the microtask queue while setTimeout uses the macrotask queue. The event loop always processes all microtasks before moving to the next macrotask.

console.log('1');
 
setTimeout(() => {
    console.log('2');
}, 0);
 
Promise.resolve().then(() => {
    console.log('3');
});
 
console.log('4');
 
// Output: 1, 4, 3, 2

Step by step:

  1. console.log('1') - Runs immediately, prints 1
  2. setTimeout - Callback sent to macrotask queue
  3. Promise.then - Callback sent to microtask queue
  4. console.log('4') - Runs immediately, prints 4
  5. Call stack empty → Process microtasks first → prints 3
  6. Microtask queue empty → Process macrotask → prints 2

What is the difference between setTimeout(fn, 0) and queueMicrotask(fn)?

setTimeout(fn, 0) puts the callback in the macrotask queue, while queueMicrotask(fn) puts it in the microtask queue. The microtask will always run first.

Use queueMicrotask when you need something to run asynchronously but as soon as possible, before any I/O or rendering.

setTimeout(() => console.log('macrotask'), 0);
queueMicrotask(() => console.log('microtask'));
console.log('sync');
 
// Output: sync, microtask, macrotask

Code Output Prediction Questions

These questions test your ability to trace through async code execution.

What is the output of this Promise and setTimeout combination?

This is a classic interview question that separates candidates who memorized answers from those who truly understand:

console.log('start');
 
setTimeout(() => console.log('timeout 1'), 0);
 
Promise.resolve()
    .then(() => {
        console.log('promise 1');
        setTimeout(() => console.log('timeout 2'), 0);
    })
    .then(() => console.log('promise 2'));
 
setTimeout(() => console.log('timeout 3'), 0);
 
console.log('end');

Output:

start
end
promise 1
promise 2
timeout 1
timeout 3
timeout 2

Explanation:

  1. Sync code runs: start, end
  2. Microtasks run: promise 1, then promise 2 (chained .then)
  3. During promise 1, a new setTimeout is queued (timeout 2)
  4. Macrotasks run in order: timeout 1, timeout 3, timeout 2

What happens with chained Promise.then callbacks?

All chained promises resolve before any macrotask because each .then adds to the microtask queue, which is fully drained before any macrotask.

setTimeout(() => console.log('timeout'), 0);
 
Promise.resolve()
    .then(() => console.log('promise 1'))
    .then(() => console.log('promise 2'))
    .then(() => console.log('promise 3'));
 
console.log('sync');

Output:

sync
promise 1
promise 2
promise 3
timeout

Each .then() callback is added to the microtask queue when the previous promise resolves, and the entire microtask queue is drained before processing the setTimeout.

What is the output when a Promise executor runs synchronously?

The Promise constructor's executor function runs synchronously—only the .then() callbacks are asynchronous:

const promise = new Promise((resolve) => {
    console.log('1');
    resolve();
    console.log('2');
});
 
promise.then(() => console.log('3'));
console.log('4');
 
// Output: 1, 2, 4, 3

The executor runs immediately when the Promise is created, so 1 and 2 print synchronously. The resolve() call queues the .then() callback as a microtask, which runs after 4.


Async/Await Questions

These questions test your understanding of how async/await works with the event loop.

How does async/await relate to the event loop?

Async/await is syntactic sugar over Promises. When you await something, the rest of the function is essentially wrapped in a .then(). So the code after await goes into the microtask queue.

async function example() {
    console.log('1');
    await Promise.resolve();
    console.log('2'); // This goes to microtask queue
}
 
example();
console.log('3');
 
// Output: 1, 3, 2

The function runs synchronously until await, then pauses. The remaining code (console.log('2')) becomes a microtask.

What is the execution order of async functions?

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
 
async function async2() {
    console.log('async2');
}
 
console.log('script start');
async1();
console.log('script end');

Output:

script start
async1 start
async2
script end
async1 end

Explanation:

  1. script start prints synchronously
  2. async1() is called, prints async1 start
  3. async2() is called, prints async2 synchronously
  4. await pauses async1, the rest becomes a microtask
  5. script end prints synchronously
  6. Call stack empty → microtask runs → async1 end prints

Node.js Event Loop Questions

These questions test your understanding of Node.js-specific event loop behavior.

How is the Node.js event loop different from the browser?

The Node.js event loop has multiple phases that process different types of callbacks. It uses the libuv library for async I/O operations.

Node.js Event Loop Phases:

  1. Timers - executes setTimeout and setInterval callbacks
  2. Pending callbacks - executes I/O callbacks deferred from previous cycle
  3. Idle, prepare - internal use only
  4. Poll - retrieves new I/O events
  5. Check - executes setImmediate callbacks
  6. Close callbacks - executes close event callbacks

Key differences: Node.js has setImmediate() (runs in check phase), process.nextTick() (runs before any other microtask), and different timing behavior for I/O operations.

What is process.nextTick in Node.js?

process.nextTick schedules a callback to run before any other microtask, even before Promise callbacks. It's specific to Node.js and can starve the event loop if used recursively.

// Node.js only
Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));
 
// Output: nextTick, promise

Generally, queueMicrotask or Promises are preferred for portability across environments.


Practical Application Questions

These questions test your ability to apply event loop knowledge to real problems.

How do you break up heavy computation to avoid blocking?

Long-running synchronous code blocks the UI. Break it into smaller chunks and yield to the event loop between chunks:

// BAD: Blocks the UI
function processLargeArray(array) {
    array.forEach(item => heavyComputation(item));
}
 
// GOOD: Yields to the event loop
function processLargeArrayAsync(array) {
    let index = 0;
 
    function processChunk() {
        const chunkSize = 100;
        const end = Math.min(index + chunkSize, array.length);
 
        while (index < end) {
            heavyComputation(array[index]);
            index++;
        }
 
        if (index < array.length) {
            setTimeout(processChunk, 0); // Yield to event loop
        }
    }
 
    processChunk();
}

Using setTimeout(fn, 0) between chunks allows the browser to handle user events and render updates.

How do you ensure DOM updates render before an alert?

The browser batches DOM changes and renders them when the call stack is empty. Using setTimeout lets the browser render first:

// BAD: Alert shows before DOM updates
button.textContent = 'Loading...';
alert('Processing!'); // Blocks - user sees old text
 
// GOOD: Let the browser render first
button.textContent = 'Loading...';
setTimeout(() => {
    alert('Processing!');
}, 0);

How does debouncing work with the event loop?

Debouncing uses setTimeout and clearTimeout to ensure a function only runs after a period of inactivity:

let timeoutId;
 
searchInput.addEventListener('input', (e) => {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => {
        // Only runs after user stops typing for 300ms
        performSearch(e.target.value);
    }, 300);
});

Each keystroke clears the previous timer and sets a new one. The search only runs when no new keystrokes occur for 300ms.


Quick Reference

ConceptWhat to Remember
Call StackLIFO, synchronous execution
Macrotask QueuesetTimeout, setInterval, I/O, UI events
Microtask QueuePromises, queueMicrotask, MutationObserver
Execution OrderSync → All Microtasks → One Macrotask → Repeat
Promise vs setTimeoutPromise always first (microtask > macrotask)
BlockingLong sync code freezes everything
async/awaitCode after await goes to microtask queue
process.nextTickNode.js only, runs before Promise callbacks

Ready to ace your interview?

Get 550+ interview questions with detailed answers in our comprehensive PDF guides.

View PDF Guides