<28/>
28 Lazy Coder
JavaScript

JavaScript Event Loop Explained: Call Stack, Web APIs & Callback Queue

Featured Image
JavaScript Event Loop Explained: Call Stack, Web APIs & Callback Queue
The Article
Table of Contents

Have you ever written code where a setTimeout set for 0 milliseconds still runs after other code that comes below it? Or noticed that a Promise always seems to “jump the line” ahead of a setTimeout, even when the timer is set to finish first?

That’s not a bug. That’s the JavaScript event loop quietly doing exactly what it’s designed to do.

The event loop is one of those topics that sounds intimidating from the name alone, but the actual idea is refreshingly simple once you see it laid out piece by piece. In this guide, we’ll build it up slowly, using a restaurant analogy you’ll be able to picture in your head, so that terms like “call stack,” “Web APIs,” and “callback queue” stop feeling like scary jargon and start feeling like common sense.

If you haven’t looked at JavaScript Promises yet, this guide will make a lot more sense once you’ve read that one first but don’t worry, we’ll still explain everything from scratch here too.

Why JavaScript Needs an Event Loop

Here’s a fact that surprises a lot of beginners: JavaScript can only do one thing at a time. This is called being single-threaded imagine a single cashier working a single register, serving one customer at a time, with no way to open a second counter.

But think about how many things a website juggles at once: waiting for a button click, fetching data from a server, running a 3-second timer, and updating the page all seemingly “at the same time.” If JavaScript can only do one thing at a time, how does none of that freeze the page while waiting?

That’s exactly the problem the event loop solves. It’s the system that lets JavaScript start a slow task, immediately move on to other work instead of sitting around waiting, and then come back to finish that slow task later once it’s actually ready all without ever needing a second cashier.

The Four Pieces of the Puzzle

Before we go step by step, let’s meet the four main players. Think of this section as introducing the characters before the story starts.

PieceWhat It Does
Call StackWhere JavaScript keeps track of the code currently running, one function at a time
Web APIsBrowser-provided helpers that handle slow tasks (timers, network requests) in the background
Callback QueueA waiting line for tasks (like setTimeout callbacks) that finished in the background and are ready to run
Event LoopThe constant checker that moves finished tasks from the queues back into the call stack

Simple analogy: Picture a busy restaurant kitchen.

Let’s look at each piece up close.

The Call Stack Explained

The call stack is a list JavaScript uses to keep track of which function is currently running, and which function called it. Every time a function is called, it gets added (“pushed”) to the top of the stack. Every time a function finishes, it gets removed (“popped”) off the top.

javascript

function greet() {
  return sayHello();
}

function sayHello() {
  return "Hello!";
}

console.log(greet());

Here’s what happens on the call stack, step by step:

  1. greet() is called → pushed onto the stack.
  2. Inside greet(), sayHello() is called → pushed on top of greet().
  3. sayHello() finishes and returns "Hello!" → popped off the stack.
  4. greet() finishes → popped off the stack.

Simple analogy: Think of the call stack like a stack of plates. You can only add a plate to the top, and you can only remove the plate that’s currently on top never one from the middle. This “last in, first out” rule is exactly how the call stack behaves.

If the call stack gets too deep (usually from a function endlessly calling itself), you get the famous “Maximum call stack size exceeded” error the plate stack has simply gotten too tall to hold.

Web APIs: Where the Waiting Happens

Here’s the part that trips people up: functions like setTimeout(), fetch(), and DOM event listeners are not actually part of the JavaScript language itself. They’re provided by the browser (or by Node.js, in server-side JavaScript), and are known as Web APIs.

When you call setTimeout(), JavaScript doesn’t sit around counting seconds. Instead, it hands the timer off to the browser’s Web API, and immediately moves on to the next line of code.

javascript

console.log("Start");

setTimeout(() => {
  console.log("This runs later");
}, 2000);

console.log("End");

Output:

Start
End
This runs later

Notice that "End" logs before "This runs later", even though setTimeout appears earlier in the code. That’s because the browser’s Web API is quietly counting down the 2 seconds in the background, while the call stack keeps moving through the rest of the script without waiting.

The Callback Queue (Task Queue)

Once a Web API finishes its background job — like a timer hitting zero, or a network request finally responding it doesn’t jump straight back into the running code. Instead, its callback function gets placed into the callback queue (also called the task queue or macrotask queue), where it waits patiently in line.

Simple analogy: Back at our restaurant, this is the “ready to plate” counter. The oven timer went off, meaning the dish is done but it still has to wait its turn until the chef (the call stack) is completely free.

This waiting is important: a task in the callback queue cannot jump into the call stack while the call stack is still busy, no matter how long it’s already been waiting.

The Microtask Queue: Promises Cut in Line

Here’s where things get interesting. Promises (from .then(), .catch(), .finally(), and async/await) don’t use the regular callback queue at all. They use a separate, higher-priority line called the microtask queue.

The rule the event loop follows is simple but important: after every single task from the call stack finishes, the entire microtask queue is fully emptied before the event loop even looks at the callback queue.

javascript

console.log("Start");

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

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

console.log("End");

Output:

Start
End
Promise callback
Timeout callback

Even though the setTimeout was set to 0 milliseconds, the Promise callback still runs first. That’s because microtasks (Promises) always get fully cleared out before the event loop is allowed to touch the callback queue (setTimeout, click events, and similar tasks).

Simple analogy: If the callback queue is the “ready to plate” counter, the microtask queue is a VIP order that the kitchen manager insists on plating immediately, every single time the chef becomes free before serving anyone waiting at the regular counter.

If you haven’t worked through Promises in detail yet, our JavaScript Promises guide is the natural companion to this one.

The Event Loop: Putting It All Together

Now we can define the event loop properly. The event loop is a constantly repeating process that follows one simple rule, over and over, for as long as your page is open:

  1. Run everything currently on the call stack, until it’s completely empty.
  2. Once empty, run every task waiting in the microtask queue, one at a time, until that queue is also empty.
  3. Take one task from the callback queue and push it onto the call stack.
  4. Go back to step 1.

That’s genuinely the whole mechanism. Every “confusing” async behavior in JavaScript why Promises seem to jump ahead, why setTimeout(fn, 0) doesn’t run instantly comes directly from following those four steps, every single time.

Walking Through a Real Example

Let’s trace through a slightly bigger example, one line at a time, to see the whole system working together.

javascript

console.log("1: Script starts");

setTimeout(() => {
  console.log("2: Timeout finished");
}, 0);

Promise.resolve()
  .then(() => console.log("3: First promise"))
  .then(() => console.log("4: Second promise"));

console.log("5: Script ends");

Here’s the play-by-play:

  1. console.log("1: Script starts") runs immediately on the call stack → logs “1: Script starts”.
  2. setTimeout hands its callback to the Web API, and moves on immediately (it does not wait, even with 0 ms).
  3. Promise.resolve().then(...) schedules its first .then() callback into the microtask queue.
  4. console.log("5: Script ends") runs immediately → logs “5: Script ends”.
  5. The call stack is now empty, so the event loop clears the microtask queue completely: it runs the first .then() (logging “3: First promise”), which schedules the second .then() and since the microtask queue must be fully drained, that one runs too, logging “4: Second promise”.
  6. Only now does the event loop check the callback queue, finds the setTimeout callback waiting there, and finally runs it → logs “2: Timeout finished”.

Final output:

1: Script starts
5: Script ends
3: First promise
4: Second promise
2: Timeout finished

If that order feels surprising at first, that’s completely normal trace through it slowly a second time, and it’ll click.

Call Stack vs Callback Queue vs Microtask Queue

ConceptWhat Lives HerePriority
Call StackCurrently executing codeRuns first, always
Microtask QueuePromise .then()/.catch()/.finally(), async/await continuationsFully emptied after every call stack run
Callback QueuesetTimeout, setInterval, DOM events, fetch completionsOnly one task taken per event loop cycle, after microtasks are empty
Web APIsWhere timers and network requests actually “wait” in the backgroundNot a queue a separate background environment

Common Mistakes Beginners Make

Interview Questions on the Event Loop

  1. Is JavaScript single-threaded or multi-threaded? JavaScript itself is single-threaded, meaning it can execute only one piece of code at a time on the call stack. Background work like timers and network requests is handled separately by the browser’s Web APIs.
  2. What’s the difference between the callback queue and the microtask queue? The callback queue holds tasks like setTimeout callbacks and DOM events, while the microtask queue holds Promise callbacks. The microtask queue is always fully emptied before the event loop processes anything from the callback queue.
  3. Why does a Promise usually run before a setTimeout(fn, 0)? Because Promise callbacks go into the microtask queue, which has higher priority and is always cleared completely before the event loop even looks at the callback queue, where setTimeout callbacks wait.
  4. What causes a “Maximum call stack size exceeded” error? It happens when the call stack grows too large, usually because of a function that calls itself (recursion) without a proper stopping condition.
  5. Are setTimeout and fetch part of the JavaScript language? No. They are Web APIs provided by the browser (or Node.js APIs on the server), not features of the core JavaScript language itself.
  6. What does the event loop actually do, in one sentence? It continuously checks whether the call stack is empty, and if it is, moves the next waiting task — first from the microtask queue, then from the callback queue — onto the call stack to be run.

Scenario-Based Problems and Solutions

Scenario 1: Your setTimeout set for 3 seconds seems to run after 5 seconds instead. Solution: The call stack was likely still busy with other synchronous code (like a heavy loop) when the timer finished. The callback has to wait in the callback queue until the call stack is completely free the timer measures the minimum wait, not a guaranteed exact time.

Scenario 2: You have both a .then() and a setTimeout(fn, 0), and you need the setTimeout to run first. Solution: You generally can’t force this directly, since microtasks always run before the callback queue is checked. If ordering truly matters, restructure the code so the dependent logic runs inside the Promise chain itself, instead of relying on race timing.

Scenario 3: Your page freezes completely after adding a chain of .then() calls. Solution: Check whether a .then() callback is scheduling another microtask that leads back into itself, creating an infinite microtask loop. Since microtasks must fully drain before anything else runs, this blocks the page just like an infinite loop would.

Scenario 4: You’re fetching data with fetch() and console.log runs before the data actually appears. Solution: This is expected fetch() is a Web API that returns a Promise immediately, while the actual response arrives later. Any code that depends on the fetched data needs to live inside a .then() block (or after an await), not directly below the fetch() call.

Frequently Asked Questions

What is the event loop in JavaScript, in one sentence?

The event loop is the mechanism that lets JavaScript run one thing at a time on the call stack, while continuously checking whether any waiting background tasks (from Web APIs) are ready to be moved in and run next.

Is the event loop part of JavaScript itself?

Not exactly. The event loop is provided by the JavaScript runtime the browser or Node.js rather than being a built-in language feature you write code with directly.

Why do Promises always run before setTimeout?

Because Promise callbacks go into the microtask queue, which the event loop always fully empties before it’s allowed to check the callback queue where setTimeout callbacks are waiting.

Does async/await work differently from the event loop?

No async/await is built entirely on top of Promises, so it follows the exact same microtask queue rules. It’s just a different, more readable way to write Promise-based code.

Can the event loop cause my page to freeze?

Yes, indirectly. If synchronous code on the call stack takes too long to finish (like a heavy loop), or if microtasks keep scheduling more microtasks endlessly, the browser can’t repaint the page or respond to clicks until that work clears.

Do all browsers implement the event loop the same way?

The core idea (call stack, task queues, microtasks) is standardized in the JavaScript specification and Web APIs, but some fine implementation details can vary slightly between browsers and Node.js.

Trusted Sources & References

This guide is grounded in official documentation. For deeper reading on any of these topics, these are reliable places to go:

We recommend bookmarking MDN Web Docs it’s the most trusted, community-maintained reference for JavaScript and the web in general.

Continue Learning

Want to build on what you just learned? Check out these related guides on 28LazyCoder:

Explore more tutorials on 28LazyCoder.

AR

Ashutosh Rajbhar

Full-stack developer

3+ years building WordPress, React, and performance-focused web projects.

Related Articles
Previous ← Custom WordPress Development: A Complete Beginner’s Guide