FroquizFroquiz
HomeQuizzesSenior ChallengeGet CertifiedBlogAbout
Sign InStart Quiz
Sign InStart Quiz
Froquiz

The most comprehensive quiz platform for software engineers. Test yourself with 10000+ questions and advance your career.

LinkedIn

Platform

  • Start Quizzes
  • Topics
  • Blog
  • My Profile
  • Sign In

About

  • About Us
  • Contact

Legal

  • Privacy Policy
  • Terms of Service

Β© 2026 Froquiz. All rights reserved.Built with passion for technology
Blog & Articles

JavaScript Event Loop Explained: Call Stack, Callbacks, Promises and async/await

Finally understand how JavaScript handles asynchronous code. Learn the call stack, event loop, task queue, microtask queue, and how Promises and async/await fit in.

Yusuf SeyitoğluMarch 11, 20265 views7 min read

JavaScript Event Loop Explained: Call Stack, Callbacks, Promises and async/await

The JavaScript event loop is one of the most misunderstood parts of the language β€” and one of the most common interview topics. Once you truly understand it, a whole category of bugs and confusing behaviors suddenly makes sense.

Let's build up the mental model from scratch.

JavaScript Is Single-Threaded

JavaScript runs on a single thread. It can only do one thing at a time. Yet we write code that fetches data, waits for timers, and handles user clicks β€” all seemingly at once.

How? The event loop.

The event loop's job: Keep checking if the call stack is empty. If it is, take the next task from the queue and push it onto the stack.

The Call Stack

The call stack is where JavaScript keeps track of what function is currently executing.

javascript
function greet(name) { return `Hello, ${name}`; } function main() { const message = greet("Alice"); console.log(message); } main();

Here is what happens on the stack:

  1. main() pushed onto stack
  2. greet("Alice") pushed onto stack
  3. greet returns β€” popped off stack
  4. console.log pushed, executes, popped
  5. main returns β€” popped off stack
  6. Stack is empty

When the stack is empty, the event loop can push the next task.

Stack overflow

If a function calls itself infinitely with no base case, the stack fills up:

javascript
function infinite() { return infinite(); // RangeError: Maximum call stack size exceeded }

Web APIs and Async Operations

When you call setTimeout, fetch, or add an event listener, you are handing work off to Web APIs β€” provided by the browser, not JavaScript itself.

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

Output: 1, 3, 2

Even with 0ms delay, the callback goes through the Web API, then the task queue, then the event loop. It can only run after the current call stack is empty.

The Task Queue (Macrotask Queue)

When async operations complete, their callbacks go into the task queue (also called the macrotask queue). The event loop picks from here only when the call stack is empty.

Macrotasks include:

  • setTimeout callbacks
  • setInterval callbacks
  • DOM events (click, keypress)
  • MessageChannel

The Microtask Queue

Promises introduced a microtask queue β€” a higher-priority queue that is drained completely before the event loop picks the next macrotask.

Microtasks include:

  • .then() / .catch() / .finally() callbacks
  • await continuations
  • queueMicrotask()
  • MutationObserver
javascript
console.log("1"); setTimeout(() => console.log("setTimeout"), 0); Promise.resolve().then(() => console.log("Promise")); console.log("2");

Output: 1, 2, Promise, setTimeout

After the synchronous code runs (1, 2), the microtask queue is drained first (Promise), then the task queue (setTimeout).

Visualizing the Full Flow

code
Call Stack +----------+ | | <-- Currently executing +----------+ ^ | Event Loop picks next task | +-----------------+ +------------------+ | Microtask Queue | | Task Queue | | (Promises etc) | | (setTimeout etc) | | Drained first | | Picked next | +-----------------+ +------------------+ ^ ^ | | +----+-------------------------+------+ | Web APIs | | (timers, fetch, event listeners) | +-------------------------------------+

Promises

Promises represent the eventual result of an async operation.

javascript
fetch("https://api.example.com/data") .then(response => response.json()) .then(data => console.log(data)) .catch(error => console.error(error));

Each .then() callback is a microtask β€” it runs after the current synchronous code but before any setTimeout.

Promise states

  • Pending β€” initial state, not yet settled
  • Fulfilled β€” operation succeeded, .then() runs
  • Rejected β€” operation failed, .catch() runs

Once settled, a Promise's state never changes.

async/await

async/await is syntactic sugar over Promises. It makes async code look and behave more like synchronous code.

javascript
async function getUser(id) { try { const response = await fetch(`/api/users/${id}`); const user = await response.json(); return user; } catch (error) { console.error("Failed:", error); } }

await pauses execution of the async function and yields control back to the event loop until the Promise settles. The function resumes as a microtask.

Common async/await mistake: forgetting await

javascript
async function bad() { const data = fetch("/api/data"); // missing await! console.log(data); // logs a Promise object, not the data }

Running Promises in parallel

javascript
// Sequential β€” slow (waits for each before starting next) const a = await fetchA(); const b = await fetchB(); // Parallel β€” fast (both start at the same time) const [a, b] = await Promise.all([fetchA(), fetchB()]);

Use Promise.all when the operations are independent of each other.

Classic Interview Questions

What is the output?

javascript
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");

Answer:

code
start end promise 1 promise 2 timeout 1 timeout 2

Synchronous code runs first (start, end). Then microtasks drain completely (promise 1, then promise 2 β€” queued when promise 1 runs). Then macrotasks (timeout 1, timeout 2).

Why does this not work as expected?

javascript
for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); } // Prints: 3, 3, 3

var is function-scoped. By the time the callbacks run, the loop is done and i is 3. Fix with let (block-scoped):

javascript
for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); } // Prints: 0, 1, 2

Summary

  • JavaScript is single-threaded β€” one thing at a time
  • The call stack tracks executing functions
  • Async work goes to Web APIs, then queues
  • Microtask queue (Promises, await) drains before the next macrotask
  • Task queue (setTimeout, events) runs after microtasks
  • async/await is syntactic sugar over Promises

Understanding the event loop makes you a significantly better JavaScript developer. You will debug async code faster and write it with more confidence.

Test your JavaScript knowledge on Froquiz β€” we cover the event loop, closures, prototypes, and much more.

About Author

Yusuf Seyitoğlu

Author β†’

Other Posts

  • CSS Advanced Techniques: Custom Properties, Container Queries, Grid Masonry and Modern LayoutsMar 12
  • GraphQL Schema Design: Types, Resolvers, Mutations and Best PracticesMar 12
  • System Design Fundamentals: Scalability, Load Balancing, Caching and DatabasesMar 12
All Blogs