The Event Loop, Visualized

Starving the loop โ€” interactive event loop example

1function feedVIPs() {
2 queueMicrotask(feedVIPs);
3}
4
5setTimeout(() => {
6 console.log('my table?!');
7}, 0);
8
9queueMicrotask(feedVIPs);

A 0ms setTimeout โ€” should run almost immediately. Watch what actually happens to it.

1 / 8

Call stack

script

Web APIs โ€” browser land

timeout cb 0ms

The event loop โ€” is the call stack empty? โ†’ drain the microtask queue first โ†’ then take one callback.

Microtask queue โ€” priority

Callback queue

Console

Fig. โ€” Starving the loop ยท step 1 of 8

What is going on here

Microtask priority is usually described as a feature. This scene shows the failure mode on the other side of it: a microtask that queues another microtask, forever. The checkpoint is defined to drain until the queue is empty, and this queue is never empty, so the checkpoint never ends.

The consequences are total. The timer callback sitting in the task queue never runs, no event handler runs, and โ€” because rendering also happens between turns โ€” the page never repaints. The tab is frozen just as thoroughly as by an infinite while loop, but far more confusingly, since the stack keeps unwinding between microtasks and nothing looks stuck.

The realistic version is not queueMicrotask calling itself. It is a recursive promise chain, or an await inside a loop whose condition never becomes false, or a resolver that reschedules itself on every settle. If you need to yield to the loop from a repeating job, the fix is to schedule the next iteration as a task โ€” setTimeout or a message channel โ€” so the checkpoint can actually finish.

Common questions

What is microtask starvation?

It is what happens when microtasks keep queueing more microtasks. The checkpoint drains until the queue is empty and never reaches that point, so tasks, event handlers and rendering are all postponed indefinitely.

Can a promise chain freeze the browser?

Yes. An endlessly self-scheduling promise chain starves the loop just as effectively as an infinite synchronous loop, even though the call stack empties between each link.

How do I yield to the event loop from a repeating job?

Schedule the next iteration as a task rather than a microtask, using setTimeout, a MessageChannel or scheduler.postTask. That lets the checkpoint finish so timers, input and rendering get their turn.

Every question on one page โ†’ยทRun your own code โ†’

Embed this figure in your own post โ†’

Drop this into any article or course page. It stays interactive โ€” readers can step through it without leaving your site.