JavaScript Event Loop: Microtasks, Macrotasks, and Execution Order
The JavaScript event loop powers how code executes asynchronously. Understanding how the call stack, microtask queue, and macrotask queue interact helps you write better, non-blocking code—and pass those tricky frontend interviews.
Key Concepts Covered:
-
Promise Resolution Order Understand how
.then()
callbacks are queued as microtasks and run beforesetTimeout
. -
Microtasks vs. Macrotasks Compare how
Promise.then
,queueMicrotask
,setTimeout
, andrequestAnimationFrame
are scheduled and executed. -
Async/Await Behavior Explore how
await
impacts execution order relative to synchronous and timed code. -
Blocking the Event Loop See how synchronous loops (like
while
) delay the execution ofsetTimeout
. -
Practical Examples Trace execution order across complex interleavings of Promises and timeouts to build a mental model of how JavaScript scheduling works.
Why This Matters:
Mastering the event loop helps developers avoid performance bottlenecks, race conditions, and timing bugs. It’s also a must-know topic for technical interviews and real-time app development.
🧵 Promise.all and the Event Loop
Promise.all
resolves when all input promises (or values) are fulfilled.
Non-promise values are treated as already resolved, but their evaluation still runs asynchronously.
This snippet also shows how setTimeout
helps observe the order of event loop execution and when the microtask queue is cleared.
const promise1 = Promise.resolve(3);
const promise2 = new Promise((resolve, reject) => {
setTimeout(resolve, 1000, "foo");
});
const promise3 = 42;
Promise.all([promise1, promise2, promise3]).then((values) => {
console.log({ values });
});
// Using setTimeout, we can execute code after the queue is empty
setTimeout(() => {
console.log("the queue is now empty");
});
const p3 = Promise.all([]); // Will be immediately resolved
const p4 = Promise.all([1337, "hi"]);
// Non-promise values are ignored, but the evaluation is done asynchronously
console.log({ p3 });
console.log({ p4 });
setTimeout(() => {
console.log({ p4 });
});
Promise.all([promise1, promise2, promise3]).then((values) => {
console.log({ values2: values });
});
const promise4 = Promise.resolve(3);
const promise5 = 42;
Promise.all([promise4, promise5]).then((values) => {
console.log({ values3: values });
});
// { p3: Promise { [] } }
// { p4: Promise { <pending> } }
// { values3: [ 3, 42 ] }
// the queue is now empty
// { p4: Promise { [ 1337, 'hi' ] } }
// { values: [ 3, 'foo', 42 ] }
// { values2: [ 3, 'foo', 42 ] }
🔄 Promise Chaining and Microtask Queue Order
Even though promise1
and promise2
are resolved immediately, their .then()
callbacks are placed in the microtask queue.
Microtasks are executed in the order they’re queued, and each .then()
forms its own chain — leading to interleaved output.
const promise1 = Promise.resolve();
const promise2 = Promise.resolve();
promise1.then(() => console.log(1)).then(() => console.log(2));
promise2.then(() => console.log(3)).then(() => console.log(4));
// 1 3 2 4
⏲️ let
in Loops with setTimeout
Using let
in a loop ensures that each iteration captures its own block-scoped value of i
.
All setTimeout
callbacks execute after 1 second, printing 0
to 3
as expected — thanks to let
’s scoping behavior.
for (let i = 0; i < 4; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
} // 0, 1, 2, 3. All after 1s
🔁 Promise Lifecycle and Event Loop Timing
This example shows the synchronous and asynchronous parts of a Promise’s lifecycle.
The executor function runs immediately, while .then()
handlers are queued as
microtasks and executed after the current call stack clears — before any setTimeout
callbacks.
const promise = new Promise((resolve, reject) => {
console.log("Promise callback");
resolve("resolved");
console.log("Promise callback end");
}).then((result) => {
console.log("Promise callback (.then)", result);
});
setTimeout(() => {
console.log("event-loop cycle: Promise (fulfilled)", promise);
}, 0);
console.log("Promise (pending)", promise);
// Promise callback
// Promise callback end
// Promise (pending) Promise { <pending> }
// Promise callback (.then) resolved
// event-loop cycle: Promise (fulfilled) Promise { undefined }
⚙️ Async Function and Timer Execution Order
Even when using await
, async
functions execute their synchronous parts immediately.
This example highlights how setTimeout
callbacks are deferred to the macrotask queue,
while synchronous code inside async
functions runs first.
async function run() {
console.log("run async");
setTimeout(() => {
console.log("run timeout");
}, 0);
}
setTimeout(() => {
console.log("timeout");
}, 0);
// await or not, same result
await run();
console.log("script");
// run async
// script
// timeout
// run timeout
⏳ Blocking the Event Loop with a While Loop
This snippet demonstrates that synchronous code (a busy while loop) can block the event loop.
Even though setTimeout
is set to execute after 500ms, the callback is delayed until the loop finishes—after roughly 2 seconds.
const seconds = new Date().getTime() / 1000;
setTimeout(() => {
// prints out "2", meaning that the callback is not called immediately after 500 milliseconds.
console.log(`Ran after ${new Date().getTime() / 1000 - seconds} seconds`);
}, 500);
while (true) {
if (new Date().getTime() / 1000 - seconds >= 2) {
console.log("Good, looped for 2 seconds");
break;
}
}
// Good, looped for 2 seconds
// Ran after 2.01 seconds
📜 Script, Microtasks, and Macrotasks in Execution Order
This example demonstrates how JavaScript prioritizes synchronous code first,
followed by microtasks (.then
, queueMicrotask
), and finally macrotasks (setTimeout
).
It reveals the precise order in which the event loop processes each queue.
console.log("Script start");
setTimeout(() => {
console.log("setTimeout");
}, 0);
Promise.resolve()
.then(() => {
console.log("Promise 1");
})
.then(() => {
console.log("Promise 2");
});
console.log("Script end");
const promise1 = new Promise((resolve, reject) => {
console.log("Promise constructor");
resolve();
}).then(() => {
console.log("Promise constructor resolve");
});
queueMicrotask(() => {
console.log("Microtask queue");
});
console.log("After Promise constructor");
// Script start
// Script end
// Promise constructor
// After Promise constructor
// Promise 1
// Promise constructor resolve
// Microtask queue
// Promise 2
// setTimeout
🕒 Blocking Inside Async Callbacks
Even though the task is scheduled with setTimeout
, once it begins, the long-running synchronous task blocks the event loop.
This illustrates how JavaScript remains single-threaded—even asynchronous calls can’t interrupt blocking operations.
function longRunningTask() {
console.log("Start Long-Running Task");
const startTime = Date.now();
while (Date.now() - startTime < 2000) {
// Simulate a long-running task (3 seconds)
}
console.log("Long-Running Task Completed");
}
function simulateNonBlocking() {
console.log("Start");
setTimeout(() => {
console.log("Non-blocking Operation");
longRunningTask();
}, 0);
console.log("End");
}
simulateNonBlocking();
// Start
// End
// Non-blocking Operation
// Start Long-Running Task
// after 2s:
// Long-Running Task Completed
🧬 Nested Microtasks in Macrotasks
Microtasks (like .then()
) always run before macrotasks (setTimeout
).
Even when a Promise is placed inside a setTimeout
, its callback becomes a microtask and runs immediately after the current macrotask finishes.
console.log("Start");
setTimeout(() => {
console.log("setTimeout 1");
Promise.resolve().then(() => {
console.log("Promise inside setTimeout 1");
});
}, 0);
setTimeout(() => {
console.log("setTimeout 2");
}, 0);
Promise.resolve()
.then(() => {
console.log("Promise 1");
})
.then(() => {
console.log("Promise 2");
});
console.log("End");
// Start
// End
// Promise 1
// Promise 2
// setTimeout 1
// Promise inside setTimeout 1
// setTimeout 2
🎥 requestAnimationFrame and Task Ordering
This example shows how different task types (setTimeout
, Promises, requestAnimationFrame
) are prioritized.
Microtasks (.then
) run before macrotasks (setTimeout
), and requestAnimationFrame
is
queued to run right before the next paint — after all other queues are cleared.
console.log("1");
setTimeout(function () {
console.log("2");
Promise.resolve().then(function () {
console.log("3");
});
}, 0);
Promise.resolve().then(function () {
console.log("4");
setTimeout(function () {
console.log("5");
}, 0);
});
requestAnimationFrame(function () {
console.log("7");
});
console.log("6");
// 1, 6, 4, 2, 3, 5, 7