Most explanations of the JavaScript event loop stop at a call stack and a callback queue. That picture is useful, but it hides the part that causes production bugs: the browser does not treat every queued callback equally, and rendering does not happen after every line of JavaScript.
A turn starts with a task
A timer callback, user interaction, parser event, or message event begins as a task. The browser runs that task to completion. JavaScript is not interrupted halfway through a synchronous function, which is why a long calculation can block input even when the rest of the application is asynchronous.
console.log('task start')
setTimeout(() => console.log('next task'), 0)
Promise.resolve().then(() => console.log('microtask'))
console.log('task end')
// task start, task end, microtask, next task
The zero-millisecond timer does not run immediately. It schedules a future task. The promise reaction enters the microtask queue, which is drained after the current stack becomes empty and before the event loop moves to another task.
Microtasks can delay the whole browser
At the end of a task, the browser performs a microtask checkpoint. It keeps draining microtasks until the queue is empty—including new microtasks created by existing microtasks. An accidental recursive promise chain can therefore starve timers, input, and painting without containing a traditional infinite loop.
function starve() {
queueMicrotask(starve)
}
starve()
This is also why splitting expensive work with Promise.resolve().then(...) does not make the page responsive. It moves work into microtasks but still prevents the event loop from reaching a rendering opportunity. Yield with a task-producing mechanism, a scheduler API, or a worker when the work is substantial.
Rendering is an opportunity, not a guarantee
After microtasks, the browser may update rendering before choosing the next task. It can skip a frame when nothing changed or when the display cadence does not require one. requestAnimationFrame callbacks run as part of the rendering lifecycle, making them appropriate for visual updates—not general background work.
The practical mental model is: run one task, empty microtasks, possibly render, then choose another task. When an interface freezes, ask which phase keeps producing work. That question is much more diagnostic than asking whether the code is asynchronous.