Q3 of 40 · JavaScript

What is the event loop and how does it differ from threads?

JavaScriptSeniorjavascript-asyncevent-looppromisesasync-awaitnode

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
}

// WHAT INTERVIEWERS LOOK FOR

Single-threaded model, the macrotask/microtask distinction with correct ordering, understanding that async/await yields without parallelism, and at least one testing implication (fake timers, Playwright vs Cypress async model, CPU-blocking hazard). Surface-level 'it's a queue' answers don't pass at senior level.

// COMMON PITFALL

Saying async/await enables parallelism. It enables concurrency (interleaving tasks cooperatively) but not parallelism (simultaneous execution on multiple CPU cores). For true parallelism in Node.js you need Worker Threads or child processes.