Skip to content
Retr0 Academy
Advanced JavaScript

Advanced JavaScript · 2 min read

Closures and the Event Loop

Lexical scope and closures, private state, the classic loop bug, and how the call stack, microtasks and macrotasks decide execution order.

Two ideas explain most "why does JavaScript do that?" moments.

Closures

The returned function keeps a reference to count after makeCounter returns.
The returned function keeps a reference to count after makeCounter returns.

A closure is a function together with the variables in scope where it was created. Those variables stay alive as long as the function does:

js

function makeCounter() {
  let count = 0;                 // private: nothing outside can touch it
  return {
    inc: () => ++count,
    get: () => count,
  };
}
const a = makeCounter(), b = makeCounter();
a.inc(); a.inc(); b.inc();
a.get(); // 2
b.get(); // 1 (each call creates a new scope)

The classic loop bug

js

for (var i = 0; i < 3; i++) setTimeout(() => console.log(i));   // 3 3 3
for (let j = 0; j < 3; j++) setTimeout(() => console.log(j));   // 0 1 2

var is function-scoped, so all three callbacks close over the same i, which is 3 when they run. let creates a fresh binding per iteration.

The event loop

Run the stack, drain ALL microtasks, then take ONE macrotask.
Run the stack, drain ALL microtasks, then take ONE macrotask.

JavaScript runs one thing at a time on a single call stack. Slow work (timers, network) is handed to the browser. When it finishes, a callback is queued:

  1. Run the current script until the stack is empty.
  2. Run every microtask: promise reactions (.then, code after await) and queueMicrotask.
  3. Render if needed.
  4. Take one macrotask (a setTimeout callback, an event, a message) and go back to 1.

Predict the output

Event loop ordering: predict, then run
<p>Predict the order, then press Run.</p>
<button id="run">Run</button>
<ol id="out"></ol>

Edit this example in the Playground.

Open in Playground

Even with a 0 ms delay, setTimeout runs after all promise callbacks, because microtasks always drain first.

Why it matters for dApps

  • await wallet.signAndSendTransaction(...) frees the thread while the user decides; the UI keeps animating.
  • React state updates, promise chains and RPC polling all follow these ordering rules.
  • Closures power hooks, event handlers, debounce and memoization.

Summary

A closure remembers its birth scope; use it for private state. The event loop runs the stack, then all microtasks, then one macrotask. With those two models you can predict any async ordering puzzle, including the ones in the final exam.