Q3 of 40 · JavaScript
What is the event loop and how does it differ from threads?
Short answer
Short answer: JavaScript is single-threaded; the event loop processes one task at a time from a queue. Unlike OS threads, there is no parallel execution — async/await and Promises don't run concurrently, they yield control to the event loop so other callbacks can run while I/O is in-flight.
Detail
The event loop is the scheduler at the heart of Node.js and the browser runtime. JavaScript has a single call stack. When the stack is empty, the event loop picks the next task (macrotask) from the task queue and runs it to completion — no preemption mid-task.
Microtasks (Promise callbacks, queueMicrotask, MutationObserver) form a separate, higher-priority queue. After each task completes, the runtime drains the entire microtask queue before picking the next macrotask. This is why Promises resolve "before" the next setTimeout, even a 0ms one.
The lifecycle: [call stack] → empty? → drain microtask queue → pick next macrotask → repeat.
How async/await fits: await suspends the current async function and returns control to the event loop — but only the awaiting function is paused, not the entire thread. When the awaited Promise resolves, the rest of the function is queued as a microtask. This is cooperative multitasking, not preemptive parallelism.
The single-threaded model means: CPU-bound work (parsing huge JSON, heavy transforms) blocks the event loop and starves other callbacks — offload to Worker Threads. I/O-bound work (HTTP requests, file reads) releases the thread while the OS handles the I/O, so thousands of concurrent connections are fine.
For test automation: Playwright's await page.click() truly awaits a Promise and returns control to the event loop between calls. Cypress queues commands synchronously then executes them asynchronously in a custom scheduler — mixing await inside cy.then() in the wrong way causes subtle ordering bugs. In Jest/Vitest, fake timers replace the native event loop timer implementation, letting you advance time without actually waiting.
// EXAMPLE
event-loop.js
// Macrotask vs microtask ordering
console.log("1 — sync");
setTimeout(() => console.log("4 — macrotask (setTimeout 0ms)"), 0);
Promise.resolve().then(() => console.log("3 — microtask (Promise)"));
console.log("2 — sync");
// Output: 1, 2, 3, 4
// Microtask fires before the next macrotask even at 0ms delay
// What async/await does under the hood
async function fetchUser(page) {
// await suspends THIS function, not the thread
// Other microtasks can run while the network request is in-flight
const response = await page.request.get("/api/user/1");
// resumed here when the Promise resolves — queued as microtask
return response.json();
}
// CPU-blocking work starves the event loop — don't do this in Node
function parseMassiveJson(str) {
return JSON.parse(str); // blocks until complete
// For large payloads, stream or move to a Worker Thread
}