Event Loop
JavaScript runs on one thread. The event loop is what lets it handle asynchronous work. It repeats the same cycle:
- take one task from the task queue and run it until the call stack is empty;
- run all microtasks, including those added along the way;
- let the browser render if needed;
- take the next task.
So the order is: synchronous code → microtasks → rendering → the next task.
Details
Parts of the Model
- Call stack: the functions that are being executed right now. See Execution Context.
- Web APIs: timers, network, DOM events. They are provided by the browser and work outside the JavaScript thread. When an operation completes, its callback is put into a queue.
- Task queue (macrotasks) and microtask queue: callbacks waiting for the call stack to become empty.
Microtasks
- Promise callbacks (
then,catch,finally); thePromiseconstructor itself executes synchronously. See Promises and async/await. - Code after
awaitin async functions queueMicrotask()MutationObservercallbacks (see Intersection, Resize and Mutation Observers)
Tasks (Macrotasks)
setTimeout,setInterval- DOM events (clicks, key presses, etc.)
- I/O and network callbacks
MessageChannel,postMessage
Rendering
Rendering is not a task. After a task and all its microtasks are done, the
browser may update the rendering (style, layout, paint) and only then takes the
next task. requestAnimationFrame callbacks run in this step, right before the
paint. See
requestAnimationFrame and requestIdleCallback.
That's why a long task or an endless chain of microtasks freezes the page. Heavy work should be split into several tasks or moved to a Web Worker.
Timers
- The delay of
setTimeoutis the minimum time, not a guarantee: the callback waits until the stack and the microtask queue are empty. setIntervalschedules calls regardless of how long the callback takes, so slow callbacks run back to back. For work that may take longer than the interval, use a recursivesetTimeout.- Both return an ID for
clearTimeout/clearInterval. An interval that is never cleared keeps its callback, and everything the callback references, in memory.
Example
console.log(1);
setTimeout(() => console.log(2), 0);
Promise.resolve().then(() => console.log(3));
queueMicrotask(() => console.log(4));
(async () => {
console.log(5);
await null;
console.log(6);
})();
console.log(7);
// 1 5 7 3 4 6 2
1,5,7: synchronous code, including the async function up to the firstawait.3,4,6: microtasks in the order they were queued.2: a task, so it runs after all microtasks.
Follow-up Questions
- Is the event loop the same in Node.js? No, it has phases and
process.nextTick. See Event Loop in Node.js. - What happens if a microtask keeps scheduling new microtasks? The loop never reaches rendering and the next task: the page freezes.
- How do you run heavy calculations without blocking the page? Split them into
chunks with
scheduler.yield()orsetTimeout, or move them to a Web Worker. See requestAnimationFrame and requestIdleCallback.
Common Mistakes
- "
setTimeout(fn, 0)runs immediately." It runs after the current code and all microtasks. - "Promises are executed in parallel." Their callbacks run on the same thread, one by one.
- "
asyncfunctions run asynchronously from the first line." The code before the firstawaitis synchronous. - "Rendering is a macrotask." It is a separate step of the loop.