Closures: private state, tiny surface

Closures: private state, tiny surface

JavaScript closures give functions memory: private state without globals, safer APIs, and predictable behavior. See what they are, when to use them, and why.

Martin Ferret

Martin Ferret

September 8, 2025

A closure is a function that keeps access to the variables from the place it was created, even after that outer function has finished. It’s the simplest way to give code “memory” without classes or globals.

Below is a small, practical example. We’ll keep the function as-is, then walk through exactly what happens line by line and why this pattern is useful in real projects.

function createCounter(initialCount = 0) {
  let count = initialCount;

  return function next() {
    count++;
    return count;
  };
}

const getNextCount = createCounter(10);
console.log(getNextCount()); // 11
console.log(getNextCount()); // 12
console.log(getNextCount()); // 13

How this works (step by step)

  1. You call createCounter(10). Inside that call, JavaScript creates a new lexical environment where count is set to 10.
  2. createCounter returns the inner function next. Crucially, next is closed over the environment where count lives. Returning the function does not discard that environment; it stays alive because next still references it.
  3. You assign the returned function to getNextCount. Now, each call to getNextCount() runs next(), which reads and updates the same hidden count variable: 11, 12, 13, and so on.
  4. Code outside cannot access count directly (count is not a property on getNextCount), which means the state is private by construction. When nothing references getNextCount anymore, the closure (and its environment) can be garbage-collected.

That’s a closure in action: function + remembered context.

Why this pattern is valuable

  • It gives you private state without globals or classes. The only way to change the state is through the function you returned, which makes behavior predictable and hard to misuse.
  • It enforces a tiny API surface. Callers get getNextCount() and nothing else; internals remain hidden.
  • It’s composable. You can create many independent counters (createCounter(0), createCounter(100)) with zero risk of them interfering.
  • It’s testable and local. State lives right next to the logic that uses it.

When to reach for a closure

Use a closure whenever logic must remember something across calls but you don’t want to expose that state:

  • Counters and toggles that persist between interactions.
  • “Run once” initializers (avoid double-booting analytics, sockets, or widgets).
  • Lightweight caches or debounced handlers that keep a timer or last result.
  • Event handlers that need to carry an ID, token, or configuration without polluting the global scope.

A few practical notes

Closures capture references to variables, not copies. Avoid capturing large objects you don’t need, and clean up listeners or timers that close over data when you’re done.

More certificates.dev articles

Get the latest news and updates on developer certifications. Content is updated regularly, so please make sure to bookmark this page or sign up to get the latest content directly in your inbox.

Looking for Certified Developers?

We can help you recruit Certified Developers for your organization or project. The team has helped many customers employ suitable resources from a pool of 100s of qualified Developers.

Let us help you get the resources you need.

Contact Us
Customer Testimonial for Hiring
like a breath of fresh air
Everett Owyoung
Everett Owyoung
Head of Talent for ThousandEyes
(a Cisco company)