JavaScript Event Loop
Learn how the call stack, microtask queue, and task queue control JavaScript execution order.
TL;DR
- The call stack executes synchronous code one frame at a time.
- Microtasks execute immediately after the current stack frame completes.
- Promises resolve before macrotasks like
setTimeoutcallbacks get processed.
The Call Stack and Macrotasks
Call stack framesTracks calls, stacking frames until each returns.
function a(){b();} function b(){console.log("b");}
a(); // a, then b, then popsSingle execution threadOnly one stack block runs at a time.
// Synchronous execution runs sequentiallySync completenessFinishes sync code before checking event queues.
console.log(1); console.log(2); // 1 before 2Stack overflowUnbounded recursion exhausts the stack.
function recurse() { return recurse(); }
// recurse(); // Maximum stack exceededMacrotask callbacksTimer, network, and user action callbacks.
setTimeout(() => console.log("macro"), 0);
console.log("sync");Single task turnsOne macrotask runs per loop turn.
setTimeout(() => console.log("1"), 0);
setTimeout(() => console.log("2"), 0);Browser paintingPaint updates happen between task queue turns.
// Repaints occur after a macrotask completesMicrotasks and Promises
Promise microtasksRuns resolve, reject, and finally callbacks inside the microtask queue.
Promise.resolve().then(() => console.log("micro"));Direct queueingSchedules low-level microtasks directly using the queueMicrotask method.
queueMicrotask(() => console.log("fast"));Full queue drainingForces all queued microtasks to run before the loop advances.
Promise.resolve().then(() => console.log("m1"));
Promise.resolve().then(() => console.log("m2"));
// m1 and m2 execute back-to-backNested microtasksProcesses nested microtasks inside the same loop iteration drain phase.
Promise.resolve().then(() => {
Promise.resolve().then(() => console.log("nested"));
});setTimeout vs Promise Ordering
Order priorityExecutes promise resolutions before timers scheduled within the same block.
setTimeout(() => console.log("time"), 0);
Promise.resolve().then(() => console.log("prom"));
// logs: prom, timeLoop interleavingRuns all pending microtasks before processing any scheduled timeout callbacks.
setTimeout(() => console.log("time"), 0);
[1, 2].forEach(n => {
Promise.resolve().then(() => console.log(n));
});
// logs: 1, 2, timerAF visual timingRuns animation callbacks before paint updates, outside standard queues.
requestAnimationFrame(() => console.log("paint"));UI Responsiveness
Execution chunkingSchedules iteration batches with setTimeout to avoid locking layout repaints.
function chunk(items, index = 0) {
const end = Math.min(index + 100, items.length);
for (let i = index; i < end; i++) { /* do work */ }
if (end < items.length) {
setTimeout(() => chunk(items, end), 0);
}
}Web WorkersDelegates heavy computations to separate threads away from main render loops.
const worker = new Worker("task.js");
worker.postMessage(data);
worker.onmessage = e => console.log(e.data);Tips
- Use the native
queueMicrotask()method to run scripts immediately after synchronous blocks but before paint updates. - Split heavy data processing into smaller
setTimeoutbatches to give browsers time to render frames.
Warnings
- Running a long synchronous loop blocks the single execution thread, freezing page interactions and rendering updates.
- Remember that recursive promise microtasks can starve macrotasks by preventing the event loop from advancing.
In Practice
Combines synchronous execution, Promise microtasks, a setTimeout timer, and async awaits to demonstrate execution priorities.
- Execute the synchronous start log statement immediately.
- Register a zero-delay timeout callback into the macrotask queue.
- Push a Promise resolution callback to the microtask queue.
- Run the async function synchronously up to the first await keyword.
- Drain the complete microtask queue before picking up the pending macrotask.
console.log("1: sync start");
setTimeout(() => {
console.log("2: setTimeout (macrotask)");
}, 0);
Promise.resolve().then(() => {
console.log("3: promise (microtask)");
});
async function run() {
console.log("4: async start (sync)");
await null;
console.log("5: async microtask");
}
run();
console.log("6: sync end");
// Output order: 1, 4, 6, 3, 5, 2FAQ
The event loop orchestrates asynchronous callbacks inside a single-threaded runtime. It polls task queues when the call stack clears and forwards pending operations to the execution thread.
Promises resolve inside the microtask queue, which has higher execution priority. The event loop drains all microtasks before picking up the next macrotask from the timer queue.
Microtasks handle Promise resolutions and direct queueMicrotask() actions. Macrotasks process callbacks from timers, user interactions, and fetch operations. Microtasks drain completely between each individual macrotask.
JavaScript runs on a single main thread. Long calculations occupy the call stack, preventing the event loop from rendering layout repaints or handling clicks. Offload heavy math to Web Workers.