Sitemap
NEHA SHARMA

NEHA SHARMA

Frontend Engineer | React • Next.js • TypeScript | AI-powered Web Applications | GitHub: github.com/NehaSharma-10 | LinkedIn: linkedin.com/in/nehasharma8529/

Event Loop Explained for Frontend Developers

7 min readJul 28, 2026

--

If you’ve ever wondered why setTimeout(fn, 0) doesn't run immediately, or what people actually mean by "non-blocking" — this one's for you. Explained the way I wish someone had explained it to me, with the diagram I wish I'd had.

First, What Problem Is the Event Loop Even Solving?

JavaScript runs on a single thread. That means it can only do one thing at a time — there’s no “background thread” quietly running your code in parallel like some other languages have.

So here’s the obvious problem: if JS can only do one thing at a time, what happens when you call something slow — like fetch() for an API, or a setTimeout for 5 seconds? Does the entire browser tab just freeze for those 5 seconds, unable to scroll, click, or render anything, until that one task finishes?

It doesn’t. And the event loop is the reason why.

Think of the event loop as the manager that makes sure tasks get executed smoothly, without one slow task blocking everything else. It’s not magic multi-threading — it’s a clever system of queues and timing that simulates doing many things at once, even on a single thread.

Non-Blocking I/O — The Core Idea

Here’s the flow, step by step, using a simple example: setTimeout(() => {...}, 5000).

Press enter or click to view image in full size
  1. Your code calls setTimeout(fn, 5000). This does not sit and wait 5 seconds in your main code.
  2. Instead, JS hands that timer off to the Web API (a browser-provided system running outside your single JS thread). “Here, time this for me, and tell me when it’s done.”
  3. Your main code keeps running immediately — nothing is blocked.
  4. After 5 seconds, the Web API is done timing. It doesn’t run your callback function directly — instead, it drops that callback into the Callback Queue (a waiting line).
  5. The Event Loop’s only job is to constantly check one thing, over and over: “Is the Call Stack empty right now?” If yes, it takes the first item from the Callback Queue and pushes it onto the Call Stack to finally run.

This is called non-blocking I/O — the “slow” work (timers, network requests, file reads) happens outside the main thread, and your code only gets interrupted to handle the result once it’s actually ready, without ever freezing while waiting.

Callback — “Call It Later”

The word callback literally breaks down as call + back (or “call later”). A callback is simply:

A function you pass as an argument into another function, which the receiving function holds onto and executes later — once a specific task, event, or timer finishes.

setTimeout(() => {
console.log("This runs later, after 5 seconds");
}, 5000);

That arrow function is the callback. You’re not running it now — you’re saying “hold onto this, and run it once the 5 seconds are up.”

This pattern is everywhere in JS: click handlers, .then(), fetch() responses, fs.readFile() in Node — all callbacks, all built on the same "call it later" idea.

Callback Hell (Pyramid of Doom)

Callbacks are great — until you need several of them to happen in sequence, each depending on the previous one’s result:

fn1(function() {
fn2(function() {
fn3(function() {
// and it keeps going...
});
});
});

Each function takes another function as its argument, which takes another, which takes another. Visually, this staircases to the right — which is exactly why it’s nicknamed the Pyramid of Doom.

When callbacks stack up like this, the code becomes messy and genuinely hard to follow — you lose track of what runs when, and error handling at each level gets repetitive. This exact pain point is why Promises and async/await were introduced later — they solve the same "wait for this, then do that" problem without the nested staircase.

Hoisting — A Quick Detour That Explains a Common “Why?”

Before going further into the event loop, there’s a related JS quirk worth understanding, because it trips people up in a similar “wait, why does this happen?” way.

console.log(a); // undefined
var a = 10;

Why does this print undefined instead of throwing an error, even though a is used before it's declared?

Here’s why: when JS runs your code, it doesn’t execute line-by-line immediately. It first goes through your code and sets up memory for every variable declared with var — assigning it a placeholder value of undefined — before actually running anything. Only after that setup pass does it start executing your code top to bottom.

So by the time console.log(a) runs, a already exists in memory (as undefined) — it just hasn't been assigned 10 yet. This "grab the memory first" behavior is called hoisting.

Get NEHA SHARMA’s stories in your inbox

Join Medium for free to get updates from this writer.

It’s a separate concept from the event loop, but both come from the same theme: JS does more setup and bookkeeping behind the scenes than it looks like from the code alone.

Event-Driven Architecture (Why Node.js Cares About This Too)

This isn’t just a browser thing. In Node.js, this same event loop model is the backbone of the entire runtime:

Every request that hits your server — a GET request, a file read, a database query — is treated as an event. Node just sits there, event-loop style, picking up tasks from its queue and handling them as they’re ready, instead of processing requests one at a time in a blocking line.

This is why Node.js can handle thousands of concurrent connections on a single thread — it’s the same non-blocking pattern, just applied at the server level instead of the browser tab level.

The Event Queue — FIFO, and Two Kinds of Events

The Event Queue (also called the Callback Queue) is where finished async tasks wait their turn to run. It works on a simple principle:

FIFO — First In, First Out. Whatever got queued first, runs first — just like a real queue/line.

Events fall into two categories:

Blocking Event Non-Blocking Event Executes fully before letting anything else run Executes itself without holding up other events Everything else waits until it’s done Other tasks can proceed while this one’s “in flight” Example: a heavy synchronous loop Example: fetch(), setTimeout(), async DB calls

The entire point of async programming in JS — Promises, callbacks, async/await — is to keep your code in the non-blocking column as much as possible, so one slow operation never freezes the whole application for everyone.

Let’s Actually Trace It — A Worked Example

Understanding the pieces is one thing. Predicting the actual output order is what really locks it in — and it’s one of the most common JS interview questions for exactly this reason.

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

What do you think this prints, in what order?

The output is: 1, 4, 3, 2 — not 1, 2, 3, 4 like the code reads top to bottom. Here's why, step by step:

  1. console.log("1") runs immediately — it's synchronous, so it executes on the spot, no queue involved. → prints 1
  2. setTimeout(..., 0) looks like it should run right away since the delay is 0. But it still gets handed off to the Web API first, and its callback goes into the Callback Queue — it does not run immediately, no matter how small the delay is.
  3. Promise.resolve().then(...) also doesn't run immediately — its callback goes into the Microtask Queue, a separate, higher-priority line.
  4. console.log("4") is synchronous too, so it runs right after "1", before either queued callback gets a turn. → prints 4
  5. Now the main code (Call Stack) is finally empty. The event loop looks for work — and it always fully empties the Microtask Queue before touching the Callback Queue. So the Promise’s .then() callback runs next. → prints 3
  6. Only after the Microtask Queue is completely empty does the event loop pull from the Callback Queue — so the setTimeout callback finally runs last. → prints 2

Final order: 1, 4, 3, 2.

The key insight worth remembering: synchronous code always finishes first, microtasks (Promises) always run before macrotasks (setTimeout/setInterval/DOM events), no matter what delay value you pass to setTimeout. A setTimeout(fn, 0) is not "run immediately" — it's "run as soon as possible, but only after the current code and all pending Promises are done."

Putting It All Together

  • JS is single-threaded — it can only run one thing at a time.
  • Slow tasks (timers, network calls) get handed off to Web APIs / Node APIs, outside the main thread.
  • When they finish, their callbacks go into the Callback Queue (FIFO).
  • The Event Loop constantly checks: is the Call Stack empty? — if yes, it pulls the next callback in and runs it.
  • Promises get a shortcut: they go into a separate Microtask Queue, which the event loop always clears before touching the regular Callback Queue — which is why Promise.then() callbacks tend to run before setTimeout callbacks, even at the same "delay."
  • Callback Hell happens when too many of these nested callbacks pile up — solved later by Promises and async/await.
  • Hoisting is a related-but-separate quirk: JS reserves memory for var declarations before running any code.

Once this clicks, a lot of confusing JS behavior — why your console.log runs before your API data arrives, why a 0ms setTimeout still runs after your current code and Promises finish (as we just traced above), why Node handles so many requests without falling over — stops feeling like magic and starts feeling like a system you can reason about.

If this helped, I’d love to hear which part clicked for you — or if there’s a JS quirk you’ve hit that still doesn’t quite make sense. Happy to dig into it in a follow-up post.

--

--

Frontend Engineer | React • Next.js • TypeScript | AI-powered Web Applications | GitHub: github.com/NehaSharma-10 | LinkedIn: linkedin.com/in/nehasharma8529/

See more from me