tryjs.app

JavaScript Closures Explained — Interactive Examples

Understand JavaScript closures with runnable counter examples. See how inner functions capture and remember their surrounding scope, and when closures cause bugs.

Closures

Functions that capture their scope

javascriptJS Fundamentals
function makeCounter(initial = 0) {
  let count = initial;
  return {
    increment: () => ++count,
    decrement: () => --count,
    value: () => count,
  };
}

const counter = makeCounter(10);
console.log(counter.increment()); // 11
console.log(counter.increment()); // 12
console.log(counter.decrement()); // 11
console.log(counter.value());     // 11

// Independent closures
const a = makeCounter();
const b = makeCounter(100);
a.increment(); a.increment();
b.increment();
console.log(a.value(), b.value()); // 2 101

// Function factory via closure
function makeAdder(x) {
  return y => x + y;
}
const add5 = makeAdder(5);
console.log(add5(3)); // 8

Overview

A closure is a function together with the lexical environment it was created in. In plain English: an inner function keeps access to the variables of the outer function even after the outer function has returned. Closures are how JavaScript implements private state, module patterns, function factories, partial application, and almost every callback-based API you've ever used.

How It Works

Lexical scoping is the foundation

In JavaScript, a function looks up variables by where it was written, not where it's called. An inner function can always read (and write) variables declared in any enclosing function.

The outer function can return — the scope lives on

When the outer function finishes, its local variables would normally be garbage-collected. But if an inner function still references them, the engine keeps that scope alive. The inner function 'closes over' those variables — hence the name.

Each call creates a fresh closure

Calling makeCounter() twice returns two independent counters with two independent count variables. They don't share state — each call produces a new lexical environment.

Closures are how private state is achieved

Variables inside the outer function are inaccessible from outside. The only way to read or mutate them is through the methods you return. This is the module pattern, and the basis of data hiding in pre-class JavaScript.

Common Mistakes

  • Creating closures in a loop with var — all callbacks share the same variable and see the final value (the classic 'all log 5' bug). Use let or const inside the loop.
  • Accidentally creating memory leaks by closing over large objects that are never released.
  • Confusing closures with 'this' — 'this' is dynamic and resolved at call time, closures capture lexical variables.
  • Expecting the inner function to see a fresh copy — closures capture references, not snapshots. If the outer variable changes, the inner function sees the new value.

When to Use It

Closures are unavoidable in JavaScript — every callback is one. Use them deliberately for private state, function factories (e.g. makeAdder(5)), memoization caches, and encapsulating module-level state without a class.

Frequently Asked Questions

What is a closure in JavaScript?

A closure is an inner function that retains access to the variables of its outer (enclosing) function, even after the outer function has returned. It's created automatically every time you define a function inside another function.

Why do closures matter?

They enable private state, callbacks with captured context, function factories, memoization, and the module pattern. In practice, every event handler and async callback you write is a closure over the surrounding scope.

Do closures cause memory leaks?

They can if you're not careful. A closure keeps its captured variables alive as long as the closure itself is referenced. If you store closures in long-lived structures that close over large objects, those objects won't be garbage-collected.

What is the 'classic closure loop bug'?

Using var in a for-loop creates a single variable shared by every iteration's callbacks — so all callbacks see the final value. The fix is to use let (which creates a new binding per iteration) or to wrap the callback in an IIFE.