|

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:

PiecePurpose
Call stackWhere sync code runs, LIFO
HeapWhere objects live
Task queue (macrotask)Callbacks from timers, I/O, events
Microtask queuePromise callbacks, queueMicrotask
Event loopMoves tasks from queues to stack

The loop, simplified:

  1. Run the current script to completion (synchronous code)
  2. Drain the microtask queue completely
  3. Pick the next macrotask from the task queue
  4. Run it to completion
  5. Drain the microtask queue again
  6. 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:

  1. sync 1 (call stack)
  2. sync 2 (call stack)
  3. promise (microtask, FIFO)
  4. microtask (microtask, FIFO)
  5. 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:

SourcePurpose
Promise.then, .catch, .finallyPromise reactions
await after a PromiseResumption of async fn
queueMicrotask(fn)Explicit microtask
MutationObserverDOM mutation callbacks
process.nextTick (Node)Node-specific

Macrotasks:

SourcePurpose
setTimeoutDelayed callback
setIntervalRepeating callback
setImmediate (Node)Next loop iteration
I/O callbacksFile reads, network
UI eventsClick, keypress
requestAnimationFrameBefore 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:

OrderType
1Sync code (call stack)
2All microtasks (drained)
3One macrotask
4All microtasks (again)
5Next 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. 1 — sync
  2. 4 — sync inside async IIFE, before await
  3. 6 — sync after the async call
  4. 3 — microtask from Promise.then
  5. 5 — microtask from await null resumption
  6. 2 — macrotask from setTimeout

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:

FeatureBrowserNode.js
setTimeoutMacrotaskMacrotask
setImmediateN/ANext iteration
process.nextTickN/ABefore microtasks
queueMicrotask
I/O callbacksEvent listenersFile, network
RenderingBetween macrotasksN/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 typeQueuePriority
Sync codeCall stack1 (highest)
process.nextTick (Node)Next-tick queue2
Promise .thenMicrotask3
queueMicrotaskMicrotask3
await resumptionMicrotask3
requestAnimationFrameBefore render4
setTimeoutMacrotask5
setIntervalMacrotask5
setImmediate (Node)Macrotask5
I/O callbacksMacrotask5

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

PriorityQueue
1Sync code (call stack)
2Microtasks (drained fully)
3One macrotask
4Microtasks again
5Next macrotask

Microtask Sources

SourceNotes
Promise.then / .catch / .finallyPromise reactions
await resumptionAfter awaited Promise
queueMicrotask(fn)Explicit
MutationObserverDOM mutations
process.nextTick (Node)Before microtasks

Macrotask Sources

SourceNotes
setTimeoutMinimum delay ~4ms
setIntervalRepeating
setImmediate (Node)Check phase
I/O callbacksAsync operations
UI eventsClick, keypress
requestAnimationFrameBefore render

Timing Functions

FunctionTypeNotes
setTimeout(fn, ms)Macrotaskms minimum ~4ms
setInterval(fn, ms)MacrotaskRepeats
queueMicrotask(fn)MicrotaskImmediate
requestAnimationFrame(fn)Render~60fps
setImmediate(fn)MacrotaskNode only

Ordering Rules

RuleMeaning
Sync firstCode runs to completion
Microtasks drain fullyAll microtasks before next macrotask
FIFO within queueOrder preserved
Microtasks can add microtasksStill drained before macrotask
One macrotask at a timeEach runs to completion

Classic Orderings

ScenarioOrder
Sync + timeoutSync, then timeout
Sync + promise + timeoutSync, promise, timeout
awaitSync before, then microtask
Nested promisesAll microtasks in order
Multiple timeoutsIn insertion order

Node vs Browser

FeatureBrowserNode.js
process.nextTick✅ (before microtasks)
setImmediate
queueMicrotask
RenderingBetween macrotasksN/A
requestAnimationFrame

Common Patterns

PatternPurpose
DebounceDelay until input stops
ThrottleRate limit
ChunkingYield between batches
BatchingGroup microtasks
QueueSerialize async work
Retry with delayBackoff

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

PitfallProblemSolution
setTimeout(0) timingDelayed ~4msUse queueMicrotask for immediate
Microtask starvationBlocks macrotasksAvoid infinite microtask loops
Blocking sync codeFreezes UIChunk or use Web Workers
Order assumptionNot guaranteedUse explicit sequencing
setInterval overlapConcurrent runsChained setTimeout
Not clearing timersMemory leaksclearTimeout
Mixing nextTick and PromisesNode-specific orderUnderstand both
Long async chainsSlow sequentialPromise.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

ConceptPriorityExample
Sync code1console.log
nextTick (Node)2process.nextTick
Microtasks3Promise.then
queueMicrotask3Immediate async
await resumption3After await
requestAnimationFrame4Before render
Macrotasks5setTimeout
I/O events5File, network
UI events5Click, 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 delay
  • await yields 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
  • requestAnimationFrame runs before rendering — use it for visuals
  • Node.js has process.nextTick (before microtasks) and setImmediate (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!