How does the JavaScript event loop work: Call Stack vs Task Queue

The JavaScript event loop is a single-threaded coordination mechanism that continuously monitors the call stack and task queue, executing synchronous code via the stack first, then dequeuing asynchronous callbacks to the stack only when it empties.

According to the h5bp/Front-end-Developer-Interview-Questions repository—specifically in src/questions/javascript-questions.md at lines 41-42—the event loop represents a fundamental concept tested in senior front-end interviews. Understanding the distinction between the call stack and task queue is essential for predicting execution order, preventing UI blocking, and debugging race conditions in modern web applications.

Core Components of the Event Loop

JavaScript’s runtime architecture relies on three primary structures working in concert to manage single-threaded execution.

The Call Stack

The call stack is a LIFO (Last-In-First-Out) data structure that tracks function execution. Every invoked function is pushed onto the stack, and the engine executes only the function at the top. When a function returns, it is popped off the stack. While the stack remains non-empty, the JavaScript engine cannot process any other work, including user input or rendering updates.

The Task Queue (Callback Queue)

The task queue (also called the callback queue or macrotask queue) holds callbacks from asynchronous operations such as setTimeout, fetch, and DOM events. When these asynchronous APIs complete, their callback functions are placed into the task queue, awaiting the stack to clear before execution.

The Event Loop Mechanism

The event loop itself is an infinite loop that performs one simple check: if the call stack is empty, it dequeues the first callback from the task queue and pushes it onto the stack for execution. This cycle repeats indefinitely, creating the illusion of concurrency while maintaining JavaScript’s single-threaded guarantee. Synchronous code always executes before any queued asynchronous callbacks.

Microtasks vs Macrotasks

While the raw analysis focuses on the task queue, modern JavaScript engines implement a microtask queue (or micro-job queue) that takes precedence over the standard task queue. Promises (Promise.then(), Promise.catch(), Promise.finally()) and queueMicrotask() place callbacks into this microtask queue.

The event loop drains the entire microtask queue before processing a single macrotask from the standard task queue. This explains why Promise resolutions execute before setTimeout callbacks, even with a timeout of zero milliseconds.

Practical Code Examples

Synchronous Execution in the Call Stack

The following example demonstrates pure synchronous execution where functions stack and unstack in LIFO order:

function a() {
  console.log('a start');
  b();
  console.log('a end');
}

function b() {
  console.log('b start');
  console.log('b end');
}

a();

Output:


a start
b start
b end
a end

The execution pushes a() onto the stack, then b() when called, completes b(), pops it, then completes a().

Asynchronous Task Queue Behavior

This example illustrates the interaction between synchronous code, microtasks, and the task queue:

console.log('script start');

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

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

console.log('script end');

Output:


script start
script end
promise microtask
timeout callback

The setTimeout callback enters the task queue, while the Promise handler enters the microtask queue. After the call stack empties (completing the synchronous logs), the event loop processes all microtasks before touching the task queue, resulting in the promise logging before the timeout.

Blocking the Event Loop

Long-running synchronous operations prevent the event loop from reaching queued tasks:

console.log('start UI work');

for (let i = 0; i < 1e9; i++) {} // Heavy synchronous loop

console.log('end UI work');

During this loop, the call stack never empties, meaning the event loop cannot process pending click events, network responses, or rendering updates, effectively freezing the browser interface until the loop terminates.

Why This Matters for Front-End Development

Understanding the event loop architecture prevents three common production issues:

  • UI Freezing: Long synchronous computations block the event loop, preventing user interactions and animations from processing until completion.
  • Race Conditions: Assuming setTimeout(fn, 0) executes immediately after current code ignores the microtask queue, leading to unexpected execution ordering when mixed with Promises.
  • Memory Leaks: Retaining references in closures scheduled for the task queue prevents garbage collection if the queue grows faster than the loop processes it.

Summary

  • The call stack executes synchronous code in LIFO order, blocking all other operations while active.
  • The task queue stores asynchronous callbacks from Web APIs, waiting for the stack to clear.
  • The event loop continuously checks if the stack is empty, then moves tasks from the queue to the stack for execution.
  • Microtasks (Promises) execute before macrotasks (setTimeout, DOM events), creating a priority system within asynchronous operations.
  • Long-running synchronous code blocks the event loop, freezing UI updates and user interactions.

Frequently Asked Questions

What happens when the call stack and task queue both contain items?

The JavaScript engine always prioritizes the call stack. The event loop only dequeues from the task queue when the call stack is completely empty. This ensures synchronous code never waits for asynchronous callbacks.

Why does setTimeout(..., 0) not execute immediately?

Even with a zero-millisecond delay, setTimeout places its callback into the task queue (macrotask queue). The event loop must wait for the current call stack to empty and for all microtasks (Promise handlers) to complete before executing the timeout callback, resulting in a minimum delay longer than zero milliseconds.

How do microtasks differ from the task queue?

Microtasks, created by Promises and queueMicrotask(), reside in a separate queue that the event loop drains completely before processing a single macrotask from the standard task queue. This means microtasks have higher priority and can potentially starve the macrotask queue if they recursively schedule more microtasks.

Can the event loop process multiple tasks simultaneously?

No. JavaScript remains single-threaded regardless of the event loop's presence. The loop coordinates which single task executes next, but it never executes two tasks at the same time. True parallelism requires Web Workers or the Worker Threads module in Node.js, which run on separate threads entirely outside the main event loop.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →