Event Loop Execution Order
Learn JavaScript event loop behavior with promises, microtasks, macrotasks, async functions, setTimeout, blocking code, and requestAnimationFrame.
JavaScript Event Loop Execution Order
The event loop is the number one topic in JavaScript interviews and one of the most important parts of the language to understand well.
These examples trace synchronous code, promise microtasks, timer macrotasks, async function continuations, blocking work, nested queues,
and requestAnimationFrame so you can predict execution order confidently.
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.
The timer log is there to make the queue order visible after the current stack and ready microtasks finish.
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 run in the order they are queued. Each .then() schedules the next link only after its own handler runs, which is why the output interleaves.
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 4let 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 1sPromise Lifecycle and Event Loop Timing
This snippet separates the synchronous and asynchronous parts of a Promise 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 snippet 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 timeoutBlocking the Event Loop with a While Loop
This snippet shows that synchronous code 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 secondsScript, Microtasks, and Macrotasks in Execution Order
Synchronous code runs first, then microtasks such as .then() and queueMicrotask(), then macrotasks such as setTimeout().
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
// setTimeoutBlocking Inside Async Callbacks
Even though the task is scheduled with setTimeout, once it begins, the long-running synchronous task blocks the event loop.
JavaScript still runs the callback on the main thread, so asynchronous scheduling does not make blocking work interruptible.
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 CompletedNested 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 2requestAnimationFrame and Task Ordering
This snippet compares setTimeout, promises, and requestAnimationFrame.
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