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 โEmbed this figure
Drop this into any article or course page. It stays interactive โ readers can step through it without leaving your site.