Beyond Code & Karma · JavaScript Internals
The invisible engine running your JavaScript — explained through cosmic order, karma, and code.
Chapter 01 — The Foundation
Before we touch the Event Loop, you need to feel this truth in your bones: JavaScript executes one line at a time, on a single thread. No parallelism. No multitasking in the traditional sense. Yet your browser downloads files, handles clicks, runs timers, and renders animations simultaneously. How?
The Cosmic Connection
In Hindu cosmology, the universe runs on a perfect, never-ending cycle — Srishti (Creation), Sthiti (Preservation), Samhara (Dissolution). Brahma creates, Vishnu maintains, Shiva dissolves. Then it begins again. Eternally.
The Event Loop is JavaScript's cosmic cycle:
"Just as the cosmic cycle is eternal and maintains the order of the universe — the Event Loop is eternal and maintains the order of your application."
Chapter 02 — Show Me The Code
This is the code that broke a million developer brains. Predict the output — then scroll down for the truth.
// What is the output order? console.log('1️⃣ Script start'); setTimeout(() => { console.log('4️⃣ setTimeout (macro)'); }, 0); Promise.resolve() .then(() => console.log('3️⃣ Promise .then (micro)')) .then(() => console.log('3b 🔸 Chained .then (micro)')); queueMicrotask(() => { console.log('3c 🔹 queueMicrotask (micro)'); }); console.log('2️⃣ Script end'); // OUTPUT: // 1️⃣ Script start // 2️⃣ Script end // 3️⃣ Promise .then (micro) // 3c 🔹 queueMicrotask (micro) // 3b 🔸 Chained .then (micro) // 4️⃣ setTimeout (macro)
0ms delay, it's a macrotask. The entire microtask queue
(all Promise callbacks) drains completely before any macrotask runs. Every. Single. Time.
All synchronous code runs immediately, top to bottom. console.log, variable assignments, function calls — all pushed and popped from the call stack right now.
Once the call stack is empty, ALL pending microtasks run — one by one. If a microtask enqueues another microtask, that runs too. This loop continues until the microtask queue is truly empty.
The Event Loop picks the oldest waiting macrotask (setTimeout callback, I/O callback, etc.), pushes it to the call stack, and runs it.
After every single macrotask, the entire microtask queue drains again before the next macrotask. This is the loop. Go to step 3. Forever.
Chapter 03 — Live Visualization
Step through a real async code scenario. Watch each function move through the Call Stack, Microtask Queue, and Macrotask Queue.
Chapter 04 — Deep Dive
This is where the 10,000 basic videos stop. Here's what actually matters at scale.
Yes. If your microtask queue never
empties — for example, a Promise .then() that always enqueues another microtask —
the macrotask queue never gets a turn. No
rendering. No user events. The browser freezes.
// 🚨 This will FREEZE your browser function infiniteMicrotasks() { Promise.resolve().then(infiniteMicrotasks); } infiniteMicrotasks(); // The macrotask queue (rendering, clicks) never runs!
Rule of Karma: What you create in the microtask queue, you must also drain. Infinite promises = infinite suffering.
Both schedule a microtask. But queueMicrotask(fn) is more explicit, doesn't create a Promise
object, and doesn't swallow errors. Promise.resolve().then(fn) has overhead — a full Promise object is
created. For high-performance hot paths, queueMicrotask is lighter.
process.nextTick() which actually runs before the
microtask queue. It's a special internal queue
— native node callbacks that have even higher
priority than Promises.
The browser Event Loop is specified by the HTML spec. Node.js uses libuv, a C++ library, which adds more phases. Node's loop has:
| Phase | What runs | Notes |
|---|---|---|
| timers | setTimeout, setInterval callbacks | After min delay |
| pending callbacks | Deferred I/O callbacks from prev cycle | Error callbacks, etc. |
| idle, prepare | Internal libuv use only | Rarely relevant |
| poll | New I/O events, network, file system | Main "work" phase |
| check | setImmediate() callbacks | After poll phase |
| close callbacks | socket.on('close', ...) | Cleanup |
process.nextTick() and Promise
microtasks run between every phase.
setImmediate() runs after the
poll phase (roughly equivalent to setTimeout(fn,
0) but more predictable in Node).
requestAnimationFrame() is neither
a microtask nor a standard macrotask. It runs
at the render step, which happens after macrotasks but
before the browser paints the next frame
(~16ms for 60fps). The order in browsers
is:
This is why rAF-based animations are smooth — they're synchronized with the browser's natural render cycle, unlike setTimeout-based animations which can drift.
async/await is syntactic sugar
over Promises — but there's subtlety. Each
await creates a suspension point.
The code after await becomes
a microtask callback. Understanding this matters
for performance.
async function fetchData() { console.log('A'); // sync — runs now await Promise.resolve(); // suspension point console.log('B'); // microtask — runs after stack clears await fetch('/api'); // suspension — waits for network console.log('C'); // microtask after network resolves } fetchData(); console.log('D'); // sync — runs BEFORE B and C // Output: A → D → B → C
Any synchronous task that runs longer than 50ms is a "Long Task" (as defined by the Chrome team). Since JS is single-threaded, a Long Task blocks: rendering, user input handling, and all queued tasks.
setTimeout(fn, 0) (yields
back to the event loop), scheduler.yield() (new browser API), Web Workers (true parallelism
for CPU-heavy work), or chunking with requestIdleCallback
for low-priority work.
// Modern way to yield back to the browser async function processLargeArray(items) { for (let i = 0; i < items.length; i++) { processItem(items[i]); // Yield every 50 items to stay responsive if (i % 50 === 0) { await new Promise(res => setTimeout(res, 0)); } } }
Chapter 05 — Quick Reference
| Task Type | API / Source | Queue | Priority | Drains |
|---|---|---|---|---|
| Promise .then/.catch | Promise | Microtask | High | After each task/microtask |
| async/await (after await) | async functions | Microtask | High | After each task/microtask |
| queueMicrotask | queueMicrotask() | Microtask | High | After each task/microtask |
| MutationObserver | DOM Mutations | Microtask | High | After each task/microtask |
| setTimeout / setInterval | Web API / libuv | Macrotask | Normal | One per event loop turn |
| setImmediate | Node.js only | Macrotask (check) | Normal | After poll phase in Node |
| I/O callbacks | fs, net, etc. | Macrotask | Normal | One per event loop turn |
| DOM click/keydown events | Browser Events | Macrotask | Normal | One per event loop turn |
| requestAnimationFrame | Browser Rendering | Render Task | Render-synced | Before browser paint |
| process.nextTick | Node.js only | nextTick Queue | Highest (Node) | Before Promises in Node |
Chapter 06 — Dharma & The Loop
In Hindu philosophy, Karma is not just "what goes around, comes around" — it's the cosmic principle that every action has a consequence, deferred to the right time. You perform an action now. The fruit of that action (Karma-phala) arrives later — when the conditions are right.
Sound familiar? Every time you write:
fetch('/api/karma').then(result => { // This is the Karma-phala — the fruit of your async action // It comes back when the universe (runtime) is ready console.log('The fruit of our async action:', result); });
You performed an action (fetch). The karma (callback) was stored in the Web API realm. When the time came (network responded), the Karma-phala (result) was placed into the Microtask Queue. The Event Loop (dharma) ensured it was delivered at the right time — not too early, not too late.
"Do not be attached to the result of your async action. Let the Event Loop handle the fruits in its own time." — Bhagavad Gita, interpreted for JavaScript developers 🙏
Chapter 07 — Test Yourself
Three questions. No cheating. Your karma is watching.
The Enlightenment