JS Event Loop Visualizer

Lab
Easy
Medium
Hard

Promise vs setTimeoutEasy

The classic interview question: who wins, a promise or a 0ms timer? Runs in your browser — nothing is sent anywhere. No classes or imports yet.

1 / 12 0ms
Call stack
  1. 1Sync
  2. 2Microtasks
  3. 3Macrotask

Your whole script runs as one task on the call stack. Synchronous code runs top to bottom without interruption.

Building blocks— drag into the editor, or click to add

Visualization

Call stack

runs one thing at a time

1
main()
your script

Microtask queue

promises · VIP lane, always first

empty

Web APIs

timers wait here, off the main thread

no timers running

Macrotask queue

timer callbacks · one per loop turn

empty

Waiting on a promise

paused awaits & pending .then()s

nothing is waiting

Console

click a line to jump to it

no output yet
The event loop, in 3 rules
  1. 1
    Synchronous code first
    Runs top to bottom on the call stack. Nothing can interrupt it.
  2. 2
    Then ALL microtasks
    Promise callbacks, await and queueMicrotask. The whole queue is emptied.
  3. 3
    Then ONE macrotask
    A timer callback runs. Then it's back to rule 2, again and again.

Learn the theory

JS Event Loop Visualizer: how it works

5 min read · Updated

JavaScript runs your code on a single thread, yet it can wait for timers, network responses and user clicks without freezing. The trick is the event loop: a small, tireless scheduler that decides what runs next. Once you understand its one rule — finish the current work, drain the microtasks, then take one task — every confusing console.log ordering puzzle becomes predictable.

In short

  • JavaScript executes one thing at a time on the call stack. Nothing can interrupt running code.
  • Timers and I/O are handled by the browser (Web APIs), which later queue callbacks.
  • After the stack empties, the loop runs all microtasks (promises, await, queueMicrotask), then one macrotask (timers, events), then repeats.
  • That is why a resolved promise always runs before setTimeout(fn, 0).
  • await does not block the program — it pauses only the surrounding async function.

The moving parts

Five pieces cooperate. The JavaScript engine only owns the first one; the rest are provided by the host (the browser or Node.js).

PieceWhat it doesWho owns it
Call stackTracks the function currently running and who called it. Last in, first out. Only one frame executes at a time.JS engine (V8, SpiderMonkey…)
Web APIsTimers, fetch, DOM events, geolocation… They run outside the stack, in parallel, and report back when done.Browser / Node runtime
Macrotask queueCallbacks from setTimeout, setInterval, UI events, MessageChannel. Also called the task or callback queue.Host
Microtask queuePromise reactions (.then, .catch, .finally), await continuations, queueMicrotask, MutationObserver.JS engine
Event loopWatches the stack. When it is empty it picks the next job from the queues according to fixed rules.Host

The algorithm, step by step

Each turn of the loop follows the same recipe:

  1. Run the current task to completion. At startup this is your whole script; later it is a single callback.
  2. Drain the microtask queue. Run every microtask until the queue is empty — including microtasks queued by other microtasks.
  3. Render if the browser needs to (style, layout, paint). This is why a long microtask chain freezes the page.
  4. Take the next macrotask from the queue and go back to step 1.
console.log('script start');

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

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

console.log('script end');

// script start
// script end
// promise        <- microtask: runs right after the script
// setTimeout     <- macrotask: waits for the next loop turn

Open the lab above and step through this example (“Promise vs setTimeout”) to see each value move between the panels.

Microtasks vs macrotasks

MicrotaskMacrotask
Examples.then() callbacks, await resumption, queueMicrotask()setTimeout, setInterval, click handlers, postMessage
When it runsRight after the current script or task, before anything elseOne per loop turn, after microtasks are drained
PriorityAlways firstAlways after microtasks
RiskAn endless microtask chain starves timers and renderingA long task blocks the UI and everything queued behind it

What async/await really does

async/await is syntax over promises. When an async function reaches await, it pauses, hands control back to its caller and registers the rest of the function as a promise reaction. When the awaited promise settles, that continuation is queued as a microtask.

async function fetchUser() {
  console.log('1: inside, before await');
  await null;                    // pauses here, even for a non-promise
  console.log('3: after await'); // runs later, as a microtask
  return 'Ada';
}

fetchUser();
console.log('2: caller keeps running');

Returning a promise from a .then() callback costs extra microtask ticks, because the engine has to adopt the inner promise. The “Returning a promise costs extra ticks” example in the lab makes those hidden hops visible.

Classic gotchas

  • `setTimeout(fn, 0)` is not instant. It means “as soon as the stack is clear and microtasks are done” — and browsers clamp nested timers to at least 4 ms.
  • `var` in loops. for (var i…) setTimeout(() => log(i)) prints the final value three times because all callbacks share one i; let creates a new binding per iteration.
  • Timers are minimum delays, not guarantees. A busy stack delays the callback.
  • Heavy synchronous work blocks everything. A 200 ms loop means no clicks, no paint and no timers for 200 ms. Split work into chunks or move it to a Web Worker.
  • Unhandled rejections surface after the microtask queue drains, which is why they appear “late” in the console.

Browser vs Node.js

Node.js uses libuv and a slightly richer loop with named phases (timers, pending callbacks, poll, check, close). Two extra tools exist there:

  • process.nextTick() runs even before promise microtasks.
  • setImmediate() runs in the “check” phase, right after I/O polling.
  • The core rule is the same: finish the current operation, drain the nextTick/microtask queues, then move to the next phase.

Frequently asked questions

Is JavaScript single-threaded?
Your JavaScript code runs on a single thread, so only one function executes at a time. The browser itself is multi-threaded: timers, network and rendering happen elsewhere and report back through the queues.
Why does a promise run before setTimeout(0)?
Promise callbacks are microtasks and timers are macrotasks. After each task the engine drains every microtask before it looks at the macrotask queue, so the promise wins even when the timer delay is zero.
What is the difference between a microtask and a macrotask?
Both are queued callbacks, but microtasks (promises, queueMicrotask) run immediately after the current script or task and are fully drained, whereas macrotasks (timers, events) run one per loop turn.
Does await block the thread?
No. await suspends only the async function it appears in. The call stack unwinds, other code runs, and the rest of the function resumes later as a microtask.
Does setTimeout(fn, 0) run immediately?
No. It schedules the callback as a macrotask, so it runs only after the current code finishes and all pending microtasks are drained — and browsers enforce a small minimum delay for nested timers.
Can I paste my own code into the visualizer?
Yes. The editor supports functions, async/await, promises, timers, loops, destructuring and try/catch. Classes and imports are not supported yet. Everything runs locally in your browser.

Keep exploring

More interactive labs

All labs

HTTP Caching Explained

Cache-Control, ETag and 304s, one request at a time.

Networking12 min · Intermediate

CORS & Preflight Simulator

Edit the request and the server headers, then see exactly why the browser blocks it.

Networking12 min · Intermediate

What Happens When You Type a URL

DNS, TCP, TLS and HTTP — with a stopwatch.

Networking10 min · Beginner

How Database Indexes Work (B-Tree)

See a B-tree lookup beat a full table scan, and why some queries ignore your index.

Databases12 min · Intermediate

What Happens When 100 Reads Hit the Database

Connection pools, CPU workers, caches and replicas, as 100 requests arrive at once.

Databases12 min · Intermediate

SQL Isolation Levels Lab

Run two transactions side by side and watch dirty reads, phantoms and write skew appear.

Databases15 min · Advanced

Sharding & Replication

Split data across servers, copy it for reads, and see what each choice costs.

Databases15 min · Advanced

Redis Commands & Data Structures

Strings, lists, hashes, sets and sorted sets, one command at a time.

Redis15 min · Beginner

Redis Caching: Cache-Aside, TTL & Eviction

Hit or miss? Watch LRU, LFU, TTLs and a cache stampede decide your database load.

Redis12 min · Intermediate

Webhooks: Delivery, Retries & Signatures

Why you get duplicate deliveries, lost events and forged requests — and how to stop them.

Queues & Jobs12 min · Intermediate

Cron Jobs: Schedules, Overlaps & Missed Runs

Write a cron expression and watch what really happens when jobs overlap or the scheduler is down.

Queues & Jobs10 min · Beginner

Job Queues with BullMQ (Redis)

Watch jobs move through waiting, active, delayed, completed and failed — and see what BullMQ stores in Redis.

Queues & Jobs14 min · Intermediate

RabbitMQ: Exchanges, Queues, Acks & Dead Letters

Direct, topic and fanout routing, competing consumers, prefetch, redelivery and dead-letter queues, drawn.

Queues & Jobs14 min · Intermediate

Git Commands, HEAD & Refs (Visual)

Every core git command drawn with arrows: three areas, commits, branches, HEAD and remotes.

Git15 min · Beginner

Git Merge vs Rebase

Watch the commit graph change under merge, rebase and squash.

Git10 min · Beginner