The Event Loop and the Concurrency Model

JavaScript runs your code on one thread — never two statements at once. Concurrency comes from queues, not threads. This lesson gives you the exact execution-order model, so "why did this log first?" stops being a guess.

Concurrency versus parallelism

Concurrency: multiple tasks are in flight — a fetch waits for the network while a click handler runs. Parallelism: two computations run at the same instant on different cores. JavaScript is concurrent by default and parallel only when you add workers (lesson 22). One thread, one call stack — and two queues of work waiting for that stack to empty.

Call stack, microtask queue, macrotask queue, and render step connected by the event loop

The event loop drains microtasks completely after every task, then runs one macrotask — and the browser renders between turns.

The two queues

Microtasks jump ahead of timers

console.log("1: script start");            // synchronous — the current task
setTimeout(() => console.log("4: timer"), 0);   // MACROtask: queued for a later turn
Promise.resolve().then(() => console.log("3: microtask")); // MICROtask: queued for the drain
console.log("2: script end");              // still the same task

// Expected output:
// 1: script start
// 2: script end
// 3: microtask    ← microtasks drain BEFORE any timer fires
// 4: timer        ← even with a 0 ms delay

Rules, in order: (1) the current synchronous task runs to completion; (2) the microtask queue drains completely — including microtasks queued by microtasks; (3) one macrotask runs (timers, I/O callbacks, event handlers); (4) repeat.

Starvation: one blocking loop freezes everything

const start = Date.now();
setTimeout(() => console.log(`fired ${Date.now() - start}ms late`), 100);

while (Date.now() - start < 300) {} // blocks the ONLY thread for 300ms

// Expected output:
// fired 302ms late — the timer could not run until the loop finished

This is the real cost of synchronous work on the main thread: not slowness, but a frozen page — clicks queue, animations stall, nothing responds. Demo async/27_event_loop_lab.js measures it.

Rendering and loop lag

The browser renders between tasks — when it can

The style → layout → paint pipeline from lesson 16 runs between event-loop turns. If tasks queue faster than they finish, renders are skipped and the page drops frames — jank. You can measure the lag directly:

// Browser context: measure how late a 0ms timer actually fires.
let last = performance.now();
setInterval(() => {
  const now = performance.now();
  const expected = 100;             // we asked for 100ms
  const lag = now - last - expected;
  console.log(`loop lag: ${lag.toFixed(1)}ms — high values mean a busy thread`);
  last = now;
}, 100);

Steady lag above a few milliseconds under idle conditions is a signal: something is blocking the thread. Lesson 23 builds the scheduling habits that fix it.

Practice: predict before you run

The task

Run async/27_event_loop_lab.js with node. For each block, write the expected order first, including which queue puts each line there. Then modify Part 3: wrap the timer's callback in await Promise.resolve() (make the callback async) and predict how the order changes before running it.

The checklist

  • You can state the four event-loop rules in order, from memory.
  • You know microtasks drain completely — including microtasks queued by microtasks.
  • You can explain jank as "tasks starving the render step", not as "slow code".
  • You can name the one situation where concurrency becomes parallelism: workers.