Back to JavaScript tutorials
Intermediate13 min read

Closures & Scope

Master lexical scoping and closures — the mechanisms behind data privacy, factories, and many design patterns.

Lexical Scope

JavaScript determines variable scope at write time, not runtime. Inner functions can access variables declared in outer scopes, forming a scope chain that the engine walks during identifier lookup.

Each function invocation creates a new execution context with its own variable environment. Understanding this model explains why two calls to the same function do not share local variables, but do share access to outer scope bindings through closures.

  • Scope is determined where code is written
  • Inner scopes can read outer bindings
  • Each call gets fresh local variables
function outer() {
  const secret = 42;
  function inner() { return secret; }
  return inner;
}
const fn = outer();
fn(); // 42

Closures in Practice

A closure is a function plus its reference to the surrounding lexical environment. Closures enable module patterns, private state, and partial application without classes.

Factory functions that return configured handlers are a common production pattern. Event listeners, debounce/throttle utilities, and middleware all rely on closures capturing configuration at creation time.

  • Functions remember their birth environment
  • Enable private variables without classes
  • Power factories and higher-order functions
function createMultiplier(factor) {
  return (n) => n * factor;
}
const triple = createMultiplier(3);
triple(5); // 15

Variable Shadowing

Inner declarations with the same name as outer bindings shadow the outer variable within that inner scope. The outer binding remains unchanged and becomes accessible again once the inner scope ends.

Shadowing is legal but can confuse readers during debugging. ESLint rules like `no-shadow` help teams catch accidental shadowing. Prefer distinct names when both bindings need to be visible in nested scopes.

  • Inner `let`/`const` hides outer binding
  • Outer value unchanged by inner assignment
  • Use lint rules to catch confusing shadowing
let count = 1;
function demo() {
  let count = 10; // shadows outer
  return count; // 10
}

Common Closure Pitfalls

The classic loop-with-`var`-and-setTimeout bug occurs because `var` is function-scoped and all callbacks share the same binding. Using `let` gives each iteration its own block-scoped binding, fixing the issue.

Closures retain references to outer variables, not snapshots of their values at closure creation time (unless you capture a copy via an IIFE or block scope). Misunderstanding this leads to bugs in async loops and event handler registration.

  • Use `let` in loops with async callbacks
  • Closures capture live bindings, not snapshots
  • IIFE pattern when block scope is unavailable
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}

Memory and Performance

Closures keep outer scope variables alive as long as the inner function is reachable. Holding closures in long-lived caches without clearing references can cause memory leaks in SPAs and Node.js services.

Profile with heap snapshots when memory grows unexpectedly. Null out references when listeners are removed, and avoid storing large objects in closures when only a small subset is needed — destructure early to limit retention.

  • Closures extend variable lifetime
  • Remove listeners to release closures
  • Avoid retaining large objects unnecessarily

Get In Touch


Ready to discuss your next project? Drop me a message.