JavaScript Closures, Scope and the Temporal Dead Zone

Understand captured bindings, block scope and initialization without relying on hoisting slogans.

What problem does this solve?

Callbacks outlive their caller. A retry handler may need configuration from an earlier request, while a shared closure can accidentally retain user data or mutable state across requests.

What is it?

A closure is a function together with access to its lexical environment. Scope determines which binding a name resolves to. The temporal dead zone is the period in a lexical binding's scope before initialization, during which reading it throws.

Mental model

A function carries a reference to the shelf where its variables live, rather than a frozen photograph of their values. Mutating a binding changes what later calls see. Separate factory calls create separate shelves.

How it works internally

Name resolution follows lexical environments determined by where code is written. let and const create block-scoped bindings; each iteration of a let loop can have its own binding. var is function-scoped. const prevents reassignment of the binding, not mutation of an object stored there. Even typeof can throw when its argument is a lexical name still in the TDZ.

Production example

A factory can capture immutable service configuration without global state. Keep request-specific objects out of process-lifetime closures unless retention is deliberate, bounded and safe.

Node.js / TypeScript implementation

Each counter has independent state. Calls share the binding within their own factory invocation.

function createCounter() {
  let count = 0;
  return () => ++count;
}
const a = createCounter();
const b = createCounter();
console.log(a(), a(), b()); // 1 2 1

Common mistakes

Capturing a loop's shared var binding can make every callback observe the final value. Reading a shadowing let before its declaration does not fall back to an outer variable. A closure can retain a large object even after the outer call returns.

Performance / reliability considerations

Closures are ordinary tools; measure memory retention rather than avoiding them categorically. Remove listeners and timers that no longer have an owner. Prefer explicit dependencies over hidden mutable state in long-lived service factories.

Interview questions

Does a closure capture a value or a binding? Why does let fix the classic loop callback example? Can a const object change? What happens when typeof reads a TDZ binding?

Quick Revision

Lexical scope follows declaration location. Closures retain access to bindings. Factory invocations create independent state. let/const are block-scoped and inaccessible before initialization; var is function-scoped. const is not deep immutability. Long-lived callbacks may keep captured objects alive.

Further reading

MDN: closures ↗

MDN: let and TDZ ↗