ZYVOPMulti-Platform Sync
SeriesAI NewsWhy ZyVOPJoin Discord
LoginGet Started
ZYVOPMulti-Platform Sync

The Developer Publishing Hub. Write once, publish everywhere, and make your work citation-ready with built-in SEO, AEO, and GEO discovery support. Zero reader paywalls.

Content

  • Categories
  • Tags
  • Badges
  • Leaderboard
  • Write Article
  • Newsletter

Company

  • About Us
  • Why ZyVOP
  • Developer API & CLI
  • Author Handbook
  • Contact

Connect

  • Privacy Policy
  • Terms of Service
  • Cookie Policy
  • DMCA Policy
  • Code of Conduct

© 2026 ZyVOP. Developer Publishing Hub.

Zero paywalls · Full content ownership
All systems operational
HomeThe JavaScript Event Loop: Why Your Code Doesn't Run the Way You Think

The JavaScript Event Loop: Why Your Code Doesn't Run the Way You Think

A practical guide to the call stack, callback queue, and microtask queue — and why promises always win against timers.

Anshu Pathak
Anshu Pathak
Senior Developer
August 25, 2026Updated August 31, 2026
4 min read
The JavaScript Event Loop: Why Your Code Doesn't Run the Way You Think
#event-loop#asynchronous programming#promises#async/await
👍1

If you've ever stared at a setTimeout(fn, 0) and wondered why it doesn't run immediately, or been surprised that a Promise resolves before a timer even though the timer was scheduled first, you've met the event loop. It's one of those concepts every JavaScript developer eventually has to confront — usually the hard way: a production bug, a failed interview question, or a late night with the debugger open.

This post breaks down what the event loop actually is, the pieces that make it up, and why understanding it will change how you write asynchronous code.

JavaScript Is Single-Threaded (Mostly)

JavaScript runs on a single thread. One line of code executes, then the next, then the next. There's no true parallelism inside your JS code itself — only ever one thing happening at a time.

That raises an obvious question: how does JavaScript handle things like network requests, file reads, or timers without freezing the entire page while it waits? The answer isn't inside the JavaScript engine at all. It's the surrounding environment — the browser or Node.js — that does the waiting, while the event loop coordinates handing work back to your single thread at the right moment.

The Four Pieces You Need to Know

1. The Call Stack This is where your code actually executes. Every function call gets pushed onto the stack, and every return pops it off. If a function calls another function, that new function goes on top. This is standard, synchronous execution, and it behaves exactly like the call stack in any other language.

2. Web APIs (or Node APIs) Things like setTimeout, fetch, and file system operations aren't part of the JavaScript language itself — they're provided by the runtime (the browser or Node). When you call one of these, the runtime takes over the waiting, freeing up the call stack to keep executing other code.

3. The Callback Queue (a.k.a. Macrotask Queue) Once a Web API finishes its work — say, a timer expires — it doesn't jump straight back into your running code. It places its callback into a queue, waiting for its turn.

4. The Microtask Queue Promises use a separate, higher-priority queue. Anything scheduled with .then(), .catch(), .finally(), or async/await goes here instead of the regular callback queue.

The Event Loop's One Job

The event loop constantly asks one question: is the call stack empty?

When the answer is yes, it checks the microtask queue first and runs everything there until it's completely empty — even if new microtasks get added along the way. Only after the microtask queue is fully drained does it pull a single task from the callback queue and push it onto the stack.

Then it repeats. Forever.

This priority ordering — microtasks fully drained before every single macrotask — is the single most important thing to internalize. It explains almost every "surprising" async behavior you'll encounter.

Seeing It In Action

Try predicting the output of this before reading the answer:

console.log('Start');

setTimeout(() => console.log('Timeout'), 0);

Promise.resolve()
  .then(() => console.log('Promise 1'))
  .then(() => console.log('Promise 2'));

console.log('End');

The output is:

Start
End
Promise 1
Promise 2
Timeout

Here's why: console.log('Start') and console.log('End') run synchronously, so they fire immediately, before anything async gets a chance. The setTimeout callback gets handed to the Web API layer, and even with a 0ms delay, it still has to wait its turn in the callback queue. Meanwhile, the promise chain queues its callbacks as microtasks, which get priority over the callback queue. So both .then() callbacks run before the timeout ever gets a chance — even though the timeout was scheduled first.

A Simplified Picture

flowchart LR
    A[Call Stack] -->|async call handed off| B[Web / Node APIs]
    B -->|timer, fetch, etc. completes| C[Callback Queue]
    B -->|promise settles| D[Microtask Queue]
    D -->|fully drained first| A
    C -->|one task, when stack + microtasks are empty| A

Node.js implements a more elaborate version of this, with additional phases for timers, I/O callbacks, and close callbacks — but the browser model above covers the core mental model that trips up most developers, regardless of runtime.

Why This Actually Matters

This isn't just trivia for interview questions (though it shows up there constantly). Understanding the event loop helps with real problems:

  • Debugging race conditions. If two async operations resolve in an unexpected order, queue priority is usually the reason.

  • Avoiding UI jank. Long synchronous blocks of code hog the call stack and block the event loop from processing anything else, including user clicks and re-renders.

  • Writing correct async/await code. await doesn't pause the whole runtime — it pauses the current function and lets the event loop keep the rest of the app running. Knowing this helps you reason about when your awaited code will actually resume relative to everything else happening.

  • Reasoning about Promise.all vs. sequential await. Once you see promises as microtasks competing for the same queue, it's much easier to predict how concurrent operations interleave.

A Common Gotcha: Starving the Callback Queue

Because microtasks are drained completely before a single macrotask runs, it's possible to accidentally starve the callback queue. If a promise's .then() keeps scheduling more microtasks, the event loop can get stuck processing them indefinitely — delaying timers, UI updates, and I/O callbacks. This is rare, but it's a real production issue, and a good reason to be thoughtful about chaining large numbers of promises recursively.

The Takeaway

The event loop isn't magic — it's a small, deterministic set of rules: run synchronous code, drain microtasks completely, then take one macrotask, and repeat. Once that model is in your head, "unexpected" async behavior stops being unexpected. You start reading async code the way the engine actually executes it, which is a genuinely useful skill whether you're debugging a stuck UI or explaining to a teammate why their timeout callback ran later than they expected.

Next time setTimeout(fn, 0) doesn't run immediately, you'll know exactly why.

Comments (2)

Login to post a comment.

Igor Ganapolsky

Igor Ganapolsky

First PostWeekend Warrior
1 week ago

Clear explanation. One practical follow-on that bites people in production: the microtask queue is drained *completely* before the next macrotask, so a promise chain that keeps scheduling more promises can starve timers and rendering entirely — an await-inside-a-recursive-function that never yields to a macrotask will hang the frame even though "nothing is blocking." If you need to break up long work, `setTimeout(fn, 0)` (or `scheduler.yield()` where available) actually yields; `await Promise.resolve()` does not. Also worth flagging that Node's loop has extra phases browsers don't: `process.nextTick` runs before promise microtasks, and `setTimeout(fn,0)` vs `setImmediate` ordering at the top level is genuinely nondeterministic — so a mental model that works in the browser can mislead you server-side.

Anshu Pathak
Anshu Pathak

Passionate developer sharing knowledge about modern web technologies and best practices.

Subscribe to Anshu Pathak's Newsletter

Direct email dispatches when new stories are published. Zero algorithms.

More from Anshu Pathak

View profile

Bloom Filters: The Data Structure That's Allowed to Lie (A Little)

Chrome and Cassandra both use Bloom filters to cheaply rule out 'definitely not here' before an expensive lookup. This post builds one from scratch, measures its actual false-positive rate against the math, and covers what it can't do, like deletion, and the newer alternatives that fix that.

5 minSep 8

Inside Praxist: The Boundary Architecture Behind an Autonomous Research Agent

Praxist keeps its core strictly separate from task-specific plugins, then runs parallel peers through a Deep Innovation Gate and quality-diversity search. This review verifies the install firsthand, checks the benchmark claims, and flags what the Fair Source license actually allows.

9 minSep 7