JavaScript 54 🧬 Event loop
console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
Promise.resolve().then(() => {
console.log('3');
});
console.log('4');
queueMicrotask(() => {
console.log('5');
});
setTimeout(() => {
console.log('6');
}, 0);
console.log('7');
async function run() {
console.log('a');
await Promise.resolve();
console.log('b');
}
run();
console.log('c');
setInterval(() => {
console.log('tick');
}, 1000);
JavaScript is single-threaded — it runs one piece of code at a time. Yet it can handle clicks, timers, network requests, and animations without freezing. The event loop is how. It’s the mechanism that lets JavaScript juggle many tasks on a single thread.
Key point: JavaScript has one call stack, one heap, and a task queue (or two). The event loop runs one task at a time — but between tasks, it drains a microtask queue that takes priority over everything else.
a – What is the event loop
The event loop is a scheduler — a loop that keeps JavaScript running by picking up tasks from queues and executing them on the call stack.
The pieces:
| Piece | Purpose |
|---|---|
| Call stack | Where sync code runs, LIFO |
| Heap | Where objects live |
| Task queue (macrotask) | Callbacks from timers, I/O, events |
| Microtask queue | Promise callbacks, queueMicrotask |
| Event loop | Moves tasks from queues to stack |
The loop, simplified:
- Run the current script to completion (synchronous code)
- Drain the microtask queue completely
- Pick the next macrotask from the task queue
- Run it to completion
- Drain the microtask queue again
- Repeat forever
Synchronous code runs first:
console.log('1');
console.log('2');
console.log('3');
// [ 1 ]
// [ 2 ]
// [ 3 ]
Nothing async yet — just the call stack.
Async callbacks wait:
console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
console.log('3');
// [ 1 ]
// [ 3 ]
// [ 2 ]
setTimeout(..., 0) doesn’t run immediately — it queues a task. The rest of the script runs first.
Microtasks have priority:
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
// [ 1 ]
// [ 4 ]
// [ 3 ]
// [ 2 ]
Even though setTimeout was scheduled first, the Promise callback runs first. Microtasks are always drained before the next macrotask.
The full ordering:
console.log('sync 1');
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise'));
queueMicrotask(() => console.log('microtask'));
console.log('sync 2');
// [ sync 1 ]
// [ sync 2 ]
// [ promise ]
// [ microtask ]
// [ timeout ]
Steps:
sync 1(call stack)sync 2(call stack)promise(microtask, FIFO)microtask(microtask, FIFO)timeout(macrotask)
Why single-threaded is fine:
JavaScript doesn’t need threads because most delays are waiting, not computing:
- Network requests: I/O-bound — waiting for data
- Timers: waiting for time to pass
- User events: waiting for clicks
The event loop waits efficiently — the OS handles the waiting; JS just processes results.
The pieces visualized:
┌──────────────────────────────────────────────┐
│ │
│ ┌──────────────────────────────────────┐ │
│ │ Call Stack │ │
│ │ run(), fn(), etc. │ │
│ └──────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ Microtask Queue │ │
│ │ promise.then, queueMicrotask │ │
│ └──────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ Macrotask Queue │ │
│ │ setTimeout, setInterval, I/O │ │
│ └──────────────────────────────────────┘ │
│ │
│ ▲ │
│ │ │
│ Event Loop │
│ (picks next task) │
│ │
└──────────────────────────────────────────────┘
Why the event loop matters:
- No race conditions — single-threaded, one task at a time
- No locks — no mutexes or semaphores needed
- Async is cheap — waiting is free
- Predictable ordering — microtasks before macrotasks
- Responsive — UI never blocks (unless you write blocking code)
The single rule:
JavaScript runs one task at a time. Between tasks, it drains all microtasks. Then it picks the next macrotask.
b – Microtasks vs macrotasks
The event loop handles two categories of tasks. Understanding the difference explains why setTimeout(0) isn’t immediate and why Promises feel faster.
Microtasks:
| Source | Purpose |
|---|---|
Promise.then, .catch, .finally | Promise reactions |
await after a Promise | Resumption of async fn |
queueMicrotask(fn) | Explicit microtask |
MutationObserver | DOM mutation callbacks |
process.nextTick (Node) | Node-specific |
Macrotasks:
| Source | Purpose |
|---|---|
setTimeout | Delayed callback |
setInterval | Repeating callback |
setImmediate (Node) | Next loop iteration |
| I/O callbacks | File reads, network |
| UI events | Click, keypress |
requestAnimationFrame | Before next paint (special) |
Microtasks run to completion before the next macrotask:
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => {
console.log('promise 1');
Promise.resolve().then(() => console.log('promise 2'));
});
console.log('sync');
// [ sync ]
// [ promise 1 ]
// [ promise 2 ]
// [ timeout ]
The event loop drains the microtask queue completely — including microtasks added during the drain — before moving to the next macrotask.
Microtask starvation:
You can block macrotasks forever by chaining microtasks:
function loop() {
Promise.resolve().then(loop); // ❌ infinite microtasks
}
loop();
// setTimeout and I/O never run
The event loop never gets to the macrotask queue because the microtask queue never empties.
setTimeout(0) isn’t 0ms:
const start = Date.now();
setTimeout(() => {
console.log('ran after', Date.now() - start, 'ms');
}, 0);
// [ ran after 1 ms ] ← usually 1–4ms
The spec says 0, but browsers clamp it to ~4ms minimum after the fifth nested call. Node.js has similar behavior.
Nested timeouts and clamping:
let i = 0;
function tick() {
console.log(i++);
if (i < 10) setTimeout(tick, 0);
}
tick();
Each setTimeout(tick, 0) is delayed by the minimum (typically 4ms). Total for 10 ticks: ~40ms.
setInterval vs chained setTimeout:
// setInterval — runs every ~100ms
setInterval(() => console.log('interval'), 100);
// Chained setTimeout — runs after each previous completes
function loop() {
console.log('loop');
setTimeout(loop, 100);
}
loop();
setInterval fires on schedule even if the previous callback hasn’t finished. Chained setTimeout waits.
The task ordering rules:
| Order | Type |
|---|---|
| 1 | Sync code (call stack) |
| 2 | All microtasks (drained) |
| 3 | One macrotask |
| 4 | All microtasks (again) |
| 5 | Next macrotask |
| … | Repeat |
queueMicrotask — explicit microtask:
console.log('1');
queueMicrotask(() => console.log('2'));
console.log('3');
// [ 1 ]
// [ 3 ]
// [ 2 ]
Useful when you need to schedule after sync code but before timers.
async/await and the event loop:
async function run() {
console.log('a');
await Promise.resolve();
console.log('b');
}
run();
console.log('c');
// [ a ]
// [ c ]
// [ b ]
await yields to the event loop. Everything after await becomes a microtask.
Detailed trace:
┌──────────────────────────────────────────────┐
│ Step 1: run() called │
│ console.log('a') │
│ await Promise.resolve() → schedules │
│ returns Promise │
│ │
│ Step 2: console.log('c') (sync) │
│ │
│ Step 3: drain microtasks │
│ resume run() at await │
│ console.log('b') │
│ │
│ Output: a, c, b │
│ │
└──────────────────────────────────────────────┘
The classic interview puzzle:
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
(async () => {
console.log('4');
await null;
console.log('5');
})();
console.log('6');
// [ 1 ]
// [ 4 ]
// [ 6 ]
// [ 3 ]
// [ 5 ]
// [ 2 ]
Breaking it down:
1— sync4— sync inside async IIFE, beforeawait6— sync after the async call3— microtask fromPromise.then5— microtask fromawait nullresumption2— macrotask fromsetTimeout
setTimeout vs queueMicrotask vs Promise.then:
setTimeout(() => console.log('timeout'), 0);
queueMicrotask(() => console.log('microtask'));
Promise.resolve().then(() => console.log('promise'));
// [ microtask ]
// [ promise ]
// [ timeout ]
Microtasks run in FIFO order, then macrotasks.
UI rendering and the event loop:
Browsers render between macrotasks, not microtasks:
loop:
drain microtasks
if (time to render):
render
pick macrotask
run it
repeat
Long-running sync code blocks rendering — that’s why heavy computation should be chunked.
requestAnimationFrame:
Fires before the next repaint — a special macrotask:
function animate() {
console.log('frame');
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
Runs at the display refresh rate (typically 60fps).
Node.js vs browser:
| Feature | Browser | Node.js |
|---|---|---|
setTimeout | Macrotask | Macrotask |
setImmediate | N/A | Next iteration |
process.nextTick | N/A | Before microtasks |
queueMicrotask | ✅ | ✅ |
| I/O callbacks | Event listeners | File, network |
| Rendering | Between macrotasks | N/A |
process.nextTick (Node) runs before Promises:
Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));
// [ nextTick ]
// [ promise ]
This is Node-specific — in the browser, it doesn’t exist.
Long tasks block everything:
// Synchronous loop blocks the event loop
function block() {
const start = Date.now();
while (Date.now() - start < 5000) {} // 5 seconds
}
block();
// No timers, no UI events, no microtasks for 5 seconds
That’s why heavy work should be chunked into smaller tasks:
function processInChunks(items, chunkSize = 1000) {
let i = 0;
function next() {
const end = Math.min(i + chunkSize, items.length);
while (i < end) {
processItem(items[i++]);
}
if (i < items.length) setTimeout(next, 0);
}
next();
}
Between chunks, the event loop gets a chance to run timers, render, and handle events.
c – Common event loop patterns
The event loop shows up in almost every pattern involving async code. Here are the ones worth knowing.
Pattern 1 — Batching DOM updates:
let pending = false;
const updates = [];
function scheduleUpdate(fn) {
updates.push(fn);
if (!pending) {
pending = true;
queueMicrotask(flush);
}
}
function flush() {
while (updates.length) updates.shift()();
pending = false;
}
scheduleUpdate(() => console.log('update 1'));
scheduleUpdate(() => console.log('update 2'));
// Both run in one microtask
Pattern 2 — Debounce with timers:
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
const save = debounce(data => console.log('saved', data), 300);
Pattern 3 — Throttle with timers:
function throttle(fn, interval) {
let last = 0;
return (...args) => {
const now = Date.now();
if (now - last >= interval) {
last = now;
fn(...args);
}
};
}
const onScroll = throttle(() => console.log('scroll'), 100);
Pattern 4 — Async chunking:
async function processLargeArray(items) {
for (let i = 0; i < items.length; i++) {
process(items[i]);
if (i % 1000 === 0) {
await new Promise(r => setTimeout(r, 0)); // yield to event loop
}
}
}
Between chunks, the event loop processes events and renders.
Pattern 5 — Async queue:
class AsyncQueue {
constructor() {
this.queue = [];
this.running = false;
}
push(task) {
this.queue.push(task);
this.run();
}
async run() {
if (this.running) return;
this.running = true;
while (this.queue.length) {
const task = this.queue.shift();
await task();
}
this.running = false;
}
}
Pattern 6 — Delaying with setTimeout(0):
async function delay(ms = 0) {
return new Promise(r => setTimeout(r, ms));
}
await delay();
// yields to the event loop
Pattern 7 — Waiting for microtasks to flush:
async function flushMicrotasks() {
await Promise.resolve();
}
flushMicrotasks().then(() => console.log('after microtasks'));
Pattern 8 — requestAnimationFrame loop:
function animate(timestamp) {
// update animation
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
Pattern 9 — Non-blocking retry:
async function retryWithBackoff(fn, attempts = 5) {
for (let i = 0; i < attempts; i++) {
try {
return await fn();
} catch (err) {
if (i === attempts - 1) throw err;
await new Promise(r => setTimeout(r, 2 ** i * 100));
}
}
}
Between retries, the event loop processes other tasks.
Pattern 10 — setImmediate vs setTimeout(0) (Node):
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
// Order depends on context
// At top level: non-deterministic
// Inside I/O: setImmediate runs first
Pattern 11 — process.nextTick recursion (Node):
function recurse(n) {
if (n <= 0) return;
process.nextTick(() => recurse(n - 1));
}
recurse(10);
All nextTick callbacks run before Promises and I/O.
Pattern 12 — Measuring task timing:
console.time('sync');
for (let i = 0; i < 1e7; i++) {}
console.timeEnd('sync');
setTimeout(() => {
console.time('async');
for (let i = 0; i < 1e7; i++) {}
console.timeEnd('async');
}, 0);
Pattern 13 — Yielding to the UI:
async function heavyComputation() {
let result = 0;
for (let i = 0; i < 1e8; i++) {
result += i;
if (i % 1e6 === 0) {
await new Promise(r => setTimeout(r, 0));
}
}
return result;
}
Pattern 14 — Microtask batching for React-like updates:
let pendingState = null;
function setState(newState) {
pendingState = { ...pendingState, ...newState };
queueMicrotask(flushState);
}
function flushState() {
if (pendingState) {
console.log('Rendering with', pendingState);
pendingState = null;
}
}
Pattern 15 — Waiting for all microtasks after a batch:
async function afterBatch() {
await new Promise(resolve => queueMicrotask(resolve));
console.log('all microtasks done');
}
Task ordering cheatsheet:
| Task type | Queue | Priority |
|---|---|---|
| Sync code | Call stack | 1 (highest) |
process.nextTick (Node) | Next-tick queue | 2 |
Promise .then | Microtask | 3 |
queueMicrotask | Microtask | 3 |
await resumption | Microtask | 3 |
requestAnimationFrame | Before render | 4 |
setTimeout | Macrotask | 5 |
setInterval | Macrotask | 5 |
setImmediate (Node) | Macrotask | 5 |
| I/O callbacks | Macrotask | 5 |
The event loop in one sentence:
Run sync code to completion. Drain all microtasks. Pick one macrotask. Run it. Drain microtasks. Repeat.
Complete Example Session
// ============================================
// PART 1: SYNC FIRST
// ============================================
console.log('1');
console.log('2');
console.log('3');
// [ 1 ]
// [ 2 ]
// [ 3 ]
// ============================================
// PART 2: SETTIMEOUT DEFERS
// ============================================
console.log('1');
setTimeout(() => console.log('2'), 0);
console.log('3');
// [ 1 ]
// [ 3 ]
// [ 2 ]
// ============================================
// PART 3: PROMISE BEFORE TIMEOUT
// ============================================
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
// [ 1 ]
// [ 4 ]
// [ 3 ]
// [ 2 ]
// ============================================
// PART 4: QUEUEMICROTASK
// ============================================
console.log('1');
queueMicrotask(() => console.log('2'));
console.log('3');
// [ 1 ]
// [ 3 ]
// [ 2 ]
// ============================================
// PART 5: AWAIT YIELDS
// ============================================
async function run() {
console.log('a');
await Promise.resolve();
console.log('b');
}
run();
console.log('c');
// [ a ]
// [ c ]
// [ b ]
// ============================================
// PART 6: MULTIPLE MICROTASKS
// ============================================
Promise.resolve().then(() => console.log('m1'));
Promise.resolve().then(() => console.log('m2'));
Promise.resolve().then(() => console.log('m3'));
// [ m1 ]
// [ m2 ]
// [ m3 ]
// ============================================
// PART 7: NESTED MICROTASKS
// ============================================
Promise.resolve().then(() => {
console.log('outer');
Promise.resolve().then(() => console.log('inner'));
});
// [ outer ]
// [ inner ]
// ============================================
// PART 8: TIMER ORDER
// ============================================
setTimeout(() => console.log('t1'), 0);
setTimeout(() => console.log('t2'), 0);
// [ t1 ]
// [ t2 ]
// ============================================
// PART 9: INTERVIEW PUZZLE
// ============================================
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
(async () => {
console.log('4');
await null;
console.log('5');
})();
console.log('6');
// [ 1 ]
// [ 4 ]
// [ 6 ]
// [ 3 ]
// [ 5 ]
// [ 2 ]
// ============================================
// PART 10: MICROTASK STARVATION
// ============================================
function starve() {
Promise.resolve().then(starve);
}
// Don't run — blocks event loop
// ============================================
// PART 11: SETTIMEOUT 0 ISN'T 0
// ============================================
const start = Date.now();
setTimeout(() => {
console.log('delay:', Date.now() - start);
}, 0);
// [ delay: 1 ] (varies)
// ============================================
// PART 12: DEBOUNCE
// ============================================
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
// ============================================
// PART 13: THROTTLE
// ============================================
function throttle(fn, interval) {
let last = 0;
return (...args) => {
const now = Date.now();
if (now - last >= interval) {
last = now;
fn(...args);
}
};
}
// ============================================
// PART 14: YIELDING TO EVENT LOOP
// ============================================
async function yieldToEventLoop() {
return new Promise(r => setTimeout(r, 0));
}
async function processInChunks(items) {
for (let i = 0; i < items.length; i++) {
if (i % 1000 === 0) await yieldToEventLoop();
}
}
// ============================================
// PART 15: QUEUEMICROTASK FOR BATCHING
// ============================================
let pending = false;
const updates = [];
function schedule(fn) {
updates.push(fn);
if (!pending) {
pending = true;
queueMicrotask(flush);
}
}
function flush() {
while (updates.length) updates.shift()();
pending = false;
}
// ============================================
// PART 16: ASYNC QUEUE
// ============================================
class AsyncQueue {
constructor() {
this.queue = [];
this.running = false;
}
push(task) {
this.queue.push(task);
this.run();
}
async run() {
if (this.running) return;
this.running = true;
while (this.queue.length) {
const task = this.queue.shift();
await task();
}
this.running = false;
}
}
// ============================================
// PART 17: REQUESTANIMATIONFRAME
// ============================================
function animate(timestamp) {
console.log('frame at', timestamp);
requestAnimationFrame(animate);
}
// requestAnimationFrame(animate);
// ============================================
// PART 18: AWAIT SEQUENCE
// ============================================
async function seq() {
console.log('1');
await null;
console.log('2');
await null;
console.log('3');
}
seq();
console.log('4');
// [ 1 ]
// [ 4 ]
// [ 2 ]
// [ 3 ]
// ============================================
// PART 19: INTERLEAVING
// ============================================
async function a() {
console.log('a1');
await null;
console.log('a2');
}
async function b() {
console.log('b1');
await null;
console.log('b2');
}
a();
b();
// [ a1 ]
// [ b1 ]
// [ a2 ]
// [ b2 ]
// ============================================
// PART 20: FULL SCRIPT
// ============================================
console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
Promise.resolve().then(() => {
console.log('3');
});
console.log('4');
queueMicrotask(() => {
console.log('5');
});
setTimeout(() => {
console.log('6');
}, 0);
console.log('7');
async function run54() {
console.log('a');
await Promise.resolve();
console.log('b');
}
run54();
console.log('c');
// Order:
// [ 1 ]
// [ 4 ]
// [ 7 ]
// [ a ]
// [ c ]
// [ 3 ]
// [ 5 ]
// [ b ]
// [ 2 ]
// [ 6 ]
Quick Reference
The Event Loop Order
| Priority | Queue |
|---|---|
| 1 | Sync code (call stack) |
| 2 | Microtasks (drained fully) |
| 3 | One macrotask |
| 4 | Microtasks again |
| 5 | Next macrotask |
Microtask Sources
| Source | Notes |
|---|---|
Promise.then / .catch / .finally | Promise reactions |
await resumption | After awaited Promise |
queueMicrotask(fn) | Explicit |
MutationObserver | DOM mutations |
process.nextTick (Node) | Before microtasks |
Macrotask Sources
| Source | Notes |
|---|---|
setTimeout | Minimum delay ~4ms |
setInterval | Repeating |
setImmediate (Node) | Check phase |
| I/O callbacks | Async operations |
| UI events | Click, keypress |
requestAnimationFrame | Before render |
Timing Functions
| Function | Type | Notes |
|---|---|---|
setTimeout(fn, ms) | Macrotask | ms minimum ~4ms |
setInterval(fn, ms) | Macrotask | Repeats |
queueMicrotask(fn) | Microtask | Immediate |
requestAnimationFrame(fn) | Render | ~60fps |
setImmediate(fn) | Macrotask | Node only |
Ordering Rules
| Rule | Meaning |
|---|---|
| Sync first | Code runs to completion |
| Microtasks drain fully | All microtasks before next macrotask |
| FIFO within queue | Order preserved |
| Microtasks can add microtasks | Still drained before macrotask |
| One macrotask at a time | Each runs to completion |
Classic Orderings
| Scenario | Order |
|---|---|
| Sync + timeout | Sync, then timeout |
| Sync + promise + timeout | Sync, promise, timeout |
await | Sync before, then microtask |
| Nested promises | All microtasks in order |
| Multiple timeouts | In insertion order |
Node vs Browser
| Feature | Browser | Node.js |
|---|---|---|
process.nextTick | ❌ | ✅ (before microtasks) |
setImmediate | ❌ | ✅ |
queueMicrotask | ✅ | ✅ |
| Rendering | Between macrotasks | N/A |
requestAnimationFrame | ✅ | ❌ |
Common Patterns
| Pattern | Purpose |
|---|---|
| Debounce | Delay until input stops |
| Throttle | Rate limit |
| Chunking | Yield between batches |
| Batching | Group microtasks |
| Queue | Serialize async work |
| Retry with delay | Backoff |
Best Practices
✅ Do This:
// Use Promise.resolve() to yield
await Promise.resolve(); // ✅
// Use queueMicrotask for immediate async
queueMicrotask(() => doWork()); // ✅
// Use setTimeout(0) for macrotask yield
await new Promise(r => setTimeout(r, 0)); // ✅
// Break long computations into chunks
for (let i = 0; i < n; i++) {
work(i);
if (i % 1000 === 0) await yieldToLoop();
} // ✅
// Use requestAnimationFrame for visuals
requestAnimationFrame(animate); // ✅
// Debounce expensive event handlers
const save = debounce(fn, 300); // ✅
// Serialize with an async queue
class AsyncQueue { ... } // ✅
❌ Don’t Do This:
// Don't rely on setTimeout 0 being immediate
setTimeout(fn, 0); // ⚠️ ~4ms minimum
// Don't block the event loop
while (Date.now() - start < 5000) {} // ❌ freezes everything
// Don't chain endless microtasks
function loop() { Promise.resolve().then(loop); } // ❌ starves macrotasks
// Don't assume order across macrotasks
setTimeout(a, 0);
setTimeout(b, 0);
// a then b in most engines, but not guaranteed
// Don't use setInterval for async work that varies
setInterval(async () => {
await longTask(); // ⚠️ overlaps
}, 100);
// Don't forget to clear timeouts
let timer = setTimeout(fn, 1000); // ✅
clearTimeout(timer); // ✅
// Don't process huge arrays synchronously
arr.forEach(heavyProcess); // ❌ blocks UI
// Don't use process.nextTick in browser
process.nextTick(fn); // ❌ browser error
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
setTimeout(0) timing | Delayed ~4ms | Use queueMicrotask for immediate |
| Microtask starvation | Blocks macrotasks | Avoid infinite microtask loops |
| Blocking sync code | Freezes UI | Chunk or use Web Workers |
| Order assumption | Not guaranteed | Use explicit sequencing |
setInterval overlap | Concurrent runs | Chained setTimeout |
| Not clearing timers | Memory leaks | clearTimeout |
Mixing nextTick and Promises | Node-specific order | Understand both |
| Long async chains | Slow sequential | Promise.all |
Real-World Examples
1. Sync First
console.log('1');
console.log('2');
// [ 1 ]
// [ 2 ]
2. Timeout Defers
console.log('1');
setTimeout(() => console.log('2'), 0);
console.log('3');
// [ 1 ]
// [ 3 ]
// [ 2 ]
3. Promise Before Timeout
setTimeout(() => console.log('t'), 0);
Promise.resolve().then(() => console.log('p'));
// [ p ]
// [ t ]
4. Await Yields
async function run() {
console.log('a');
await null;
console.log('b');
}
run();
console.log('c');
// [ a ]
// [ c ]
// [ b ]
5. Multiple Microtasks
Promise.resolve().then(() => console.log('1'));
Promise.resolve().then(() => console.log('2'));
Promise.resolve().then(() => console.log('3'));
// [ 1 ]
// [ 2 ]
// [ 3 ]
6. Nested Microtasks
Promise.resolve().then(() => {
console.log('outer');
Promise.resolve().then(() => console.log('inner'));
});
// [ outer ]
// [ inner ]
7. Interview Puzzle
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
(async () => {
console.log('4');
await null;
console.log('5');
})();
console.log('6');
// [ 1 ]
// [ 4 ]
// [ 6 ]
// [ 3 ]
// [ 5 ]
// [ 2 ]
8. Timer Delay
const start = Date.now();
setTimeout(() => console.log(Date.now() - start), 0);
// [ 1 ] (or more)
9. Debounce
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
10. Throttle
function throttle(fn, interval) {
let last = 0;
return (...args) => {
const now = Date.now();
if (now - last >= interval) {
last = now;
fn(...args);
}
};
}
11. Chunking
async function processLarge(items) {
for (let i = 0; i < items.length; i++) {
process(items[i]);
if (i % 1000 === 0) {
await new Promise(r => setTimeout(r, 0));
}
}
}
12. queueMicrotask
console.log('1');
queueMicrotask(() => console.log('2'));
console.log('3');
// [ 1 ]
// [ 3 ]
// [ 2 ]
13. Async Queue
class AsyncQueue {
constructor() {
this.queue = [];
this.running = false;
}
push(task) {
this.queue.push(task);
this.run();
}
async run() {
if (this.running) return;
this.running = true;
while (this.queue.length) await this.queue.shift()();
this.running = false;
}
}
14. requestAnimationFrame
function animate(t) {
console.log('frame at', t);
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
15. Interleaving
async function a() {
console.log('a1');
await null;
console.log('a2');
}
async function b() {
console.log('b1');
await null;
console.log('b2');
}
a();
b();
// [ a1 ]
// [ b1 ]
// [ a2 ]
// [ b2 ]
16. Microtask Starvation
// NEVER do this
function loop() {
Promise.resolve().then(loop);
}
// loop();
17. Clear Timeout
const timer = setTimeout(() => console.log('x'), 1000);
clearTimeout(timer); // cancelled
18. Node nextTick
// Node only
process.nextTick(() => console.log('nextTick'));
Promise.resolve().then(() => console.log('promise'));
// [ nextTick ]
// [ promise ]
19. setImmediate (Node)
setImmediate(() => console.log('immediate'));
setTimeout(() => console.log('timeout'), 0);
// Order varies at top level
20. Full Script
console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
Promise.resolve().then(() => {
console.log('3');
});
console.log('4');
queueMicrotask(() => {
console.log('5');
});
setTimeout(() => {
console.log('6');
}, 0);
console.log('7');
async function run54() {
console.log('a');
await Promise.resolve();
console.log('b');
}
run54();
console.log('c');
// Order:
// [ 1 ]
// [ 4 ]
// [ 7 ]
// [ a ]
// [ c ]
// [ 3 ]
// [ 5 ]
// [ b ]
// [ 2 ]
// [ 6 ]
Visual: The Event Loop
┌──────────────────────────────────────────────┐
│ │
│ ┌─────────────────────┐ │
│ │ Call Stack │ │
│ │ (sync code) │ │
│ └──────────┬──────────┘ │
│ │ │
│ │ empty? │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ Microtask Queue │ ← drain fully │
│ │ Promise, await, │ │
│ │ queueMicrotask │ │
│ └──────────┬──────────┘ │
│ │ │
│ │ empty? │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ Macrotask Queue │ ← pick ONE │
│ │ setTimeout, events │ │
│ └──────────┬──────────┘ │
│ │ │
│ └──────────────► repeat │
│ │
└──────────────────────────────────────────────┘
Visual: Priority Order
┌──────────────────────────────────────────────┐
│ Highest priority │
│ │
│ 1. Sync code (call stack) │
│ │
│ 2. process.nextTick (Node) │
│ │
│ 3. Microtasks │
│ • Promise.then/.catch/.finally │
│ • await resumption │
│ • queueMicrotask │
│ • MutationObserver │
│ │
│ 4. requestAnimationFrame (before render) │
│ │
│ 5. Macrotasks │
│ • setTimeout │
│ • setInterval │
│ • setImmediate (Node) │
│ • I/O callbacks │
│ • UI events │
│ │
│ Lowest priority │
│ │
└──────────────────────────────────────────────┘
Visual: Sync vs Async Timeline
┌──────────────────────────────────────────────┐
│ Time → │
│ │
│ ┌────┐ ┌────┐ ┌────┐ │
│ │sync│ │sync│ │sync│ ← run now │
│ └────┘ └────┘ └────┘ │
│ │
│ ────────────► │ │
│ │ │
│ ┌──────┴──────┐ │
│ │ microtasks │ ← drain fully │
│ └──────┬──────┘ │
│ │ │
│ ┌──────┴──────┐ │
│ │ macrotask │ ← one at a time │
│ └──────┬──────┘ │
│ │ │
│ ┌──────┴──────┐ │
│ │ microtasks │ ← drain again │
│ └─────────────┘ │
│ │
└──────────────────────────────────────────────┘
Visual: Why setTimeout(0) Isn’t 0
┌──────────────────────────────────────────────┐
│ setTimeout(() => log('x'), 0) │
│ │
│ 1. Schedule as macrotask │
│ 2. Return immediately │
│ 3. Event loop processes microtasks │
│ 4. Event loop picks the macrotask │
│ 5. Actual delay: ≥ 4ms (clamped) │
│ │
│ Not really 0 — a minimum │
│ │
└──────────────────────────────────────────────┘
Summary
| Concept | Priority | Example |
|---|---|---|
| Sync code | 1 | console.log |
nextTick (Node) | 2 | process.nextTick |
| Microtasks | 3 | Promise.then |
queueMicrotask | 3 | Immediate async |
await resumption | 3 | After await |
requestAnimationFrame | 4 | Before render |
| Macrotasks | 5 | setTimeout |
| I/O events | 5 | File, network |
| UI events | 5 | Click, keypress |
Key takeaways:
- JavaScript is single-threaded — one task at a time
- The event loop picks tasks from queues and runs them on the stack
- Sync code runs first — always
- Microtasks (
Promise,await,queueMicrotask) run before the next macrotask — all of them - Macrotasks (
setTimeout,setInterval, I/O, events) run one at a time setTimeout(fn, 0)isn’t immediate — it’s a minimum ~4ms delayawaityields to the event loop — code after it runs as a microtask- Never block the event loop — chunk long computations
- Never starve macrotasks with infinite microtasks
requestAnimationFrameruns before rendering — use it for visuals- Node.js has
process.nextTick(before microtasks) andsetImmediate(macrotask) - Understanding this ordering explains every async bug you’ll ever see
Remember: The event loop is JavaScript’s concurrency model. One stack, three queues, one loop. Sync first. Microtasks drain fully. Then one macrotask. Promise.then beats setTimeout(0). await yields. Blocking code freezes everything. Master the event loop, and async JavaScript stops being magic — it becomes a predictable sequence you can reason about.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!