JavaScript Event Loop
Learn how the call stack, microtask queue, and task queue control JavaScript execution order.
TL;DR
- 01The call stack executes synchronous code one frame at a time.
- 02Microtasks execute immediately after the current stack frame completes.
- 03Promises resolve before macrotasks like
setTimeoutcallbacks get processed.
Tips
- 01Use the native
queueMicrotask()method to run scripts immediately after synchronous blocks but before paint updates. - 02Split heavy data processing into smaller
setTimeoutbatches to give browsers time to render frames.
Warnings
- 01Running a long synchronous loop blocks the single execution thread, freezing page interactions and rendering updates.
- 02Remember that recursive promise microtasks can starve macrotasks by preventing the event loop from advancing.
The Call Stack
Call stack framesTracks function calls in progress, stacking execution frames until returns occur.
function a() { b(); }
function b() { console.log("b"); }
a(); // pushes a, then b, then popsSingle execution threadAllows only one stack block to run at any single time.
// Synchronous execution runs sequentiallySync completenessProcesses synchronous scripts completely before checking background event queues.
console.log(1);
console.log(2);
// 1 always logs before 2Stack overflowTriggers errors when unbounded recursive calls consume all stack memory.
function recurse() { return recurse(); }
// recurse(); // Maximum stack exceededMacrotasks and the Task Queue
Macrotask callbacksIncludes timer callbacks, network input, and user action triggers.
setTimeout(() => console.log("macro"), 0);
console.log("sync");Single task turnsExecutes exactly one macrotask per event loop rotation iteration.
setTimeout(() => console.log("1"), 0);
setTimeout(() => console.log("2"), 0);
// separate event loop iterationsBrowser paintingInterleaves browser paint updates between subsequent 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);In Practice
Combines synchronous execution, Promise microtasks, a setTimeout timer, and async awaits to demonstrate execution priorities.
- 01Execute the synchronous start log statement immediately.
- 02Register a zero-delay timeout callback into the macrotask queue.
- 03Push a Promise resolution callback to the microtask queue.
- 04Run the async function synchronously up to the first await keyword.
- 05Drain 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.
No, a zero delay registers a callback into the macrotask queue. The engine must finish executing the current call stack and empty the microtask queue before starting it.