BitlystBitlyst
PostsSeriesTopicsAbout

Bitlyst

Short, diagram-first posts on JavaScript, React & Next.js. Follow via RSS or the newsletter.

© 2026 Mohsen Fallahnejad

PostsSeriesTopicsLinksAboutRSSGitHubX
Posts/javascript
Intermediate5 min read

JavaScript Closures — A Simple Deep Dive

What closures are, how they work (lexical scope), and how to use them without pitfalls.

Mohsen Fallahnejad

Mohsen Fallahnejad

Sep 23, 2025Discuss

TL;DR#

A closure is when a function remembers variables from its outer scope even after that outer function has finished running.

Think: “The inner function has a backpack with variables it captured when it was created.”


The Core Idea (Lexical Scope)#

  • JavaScript uses lexical (static) scoping: what you can access is determined by where functions are defined in the code (not where they’re called).
  • When you create a function, JS binds a hidden link to its Lexical Environment (scope chain). That link is carried with the function forever → this is the closure.
js
function makeCounter() {
  let count = 0;                 // captured variable
  return function () {           // inner function forms a closure over `count`
    count++;
    return count;
  };
}

const next = makeCounter();
console.log(next()); // 1
console.log(next()); // 2  (still remembers `count`)

The makeCounter frame is gone, but the inner function keeps count alive through its closure.


Why Closures Are Useful#

1.Data privacy / encapsulation

js
function createBankAccount() {
  let balance = 0;
  return {
    deposit(x) { balance += x; },
    withdraw(x) { if (x <= balance) balance -= x; },
    getBalance() { return balance; }
  };
}
const acc = createBankAccount();
acc.deposit(100);
console.log(acc.getBalance()); // 100 (no direct write access)

2.Callbacks & async

js
function greetLater(name) {
  setTimeout(() => console.log("Hello", name), 500); // `name` is closed over
}

3.Function factories / partial application

js
const by = key => (a, b) => (a[key] > b[key] ? 1 : -1);
users.sort(by("age"));

4.Memoization

js
function memoize(fn) {
  const cache = new Map();
  return x => cache.has(x) ? cache.get(x) : (cache.set(x, fn(x)), cache.get(x));
}

Common Gotchas#

1) Loops with var vs let#

js
for (var i = 1; i <= 3; i++) {
  setTimeout(() => console.log("var:", i), 0);
}
// var: 4, var: 4, var: 4  (one shared `i`)

for (let j = 1; j <= 3; j++) {
  setTimeout(() => console.log("let:", j), 0);
}
// let: 1, let: 2, let: 3  (new binding per iteration)
  • var is function-scoped → the same i is captured.
  • let/const are block-scoped → each iteration creates a fresh binding.

2) Stale closures#

js
function makeAdder(n) {
  return x => x + n; // captures `n` at creation time
}
const add10 = makeAdder(10);
console.log(add10(5)); // 15
// If `n` changes later in outer scope (different ref), this `add10` still uses the captured one.

3) Memory considerations#

  • Closures keep referenced variables alive. Large objects in closures can extend memory lifetime.
  • Avoid capturing unnecessary objects; null them out or restructure if needed.

Visualizing the Closure (Mental Model)#

  • When function () { ... } is created, JS binds a hidden pointer to its outer environment.
  • When the outer function returns, that environment may stay in memory if any inner function still references it.
  • Each call to the outer function creates a new environment (so independent counters, etc.).

Patterns & Recipes#

Module pattern#

js
const counterModule = (function () {
  let value = 0;
  return {
    inc() { value++; },
    dec() { value--; },
    get() { return value; }
  };
})();

Once-only#

js
function once(fn) {
  let called = false, result;
  return (...args) => {
    if (!called) { called = true; result = fn(...args); }
    return result;
  };
}

Event handler factories#

js
function makeHandler(prefix) {
  return e => console.log(prefix, e.target.id);
}
button.addEventListener("click", makeHandler("Clicked:"));

Closures in Frameworks#

React#

  • Every render creates a new function with its own closed-over values.
  • Handlers can capture stale state. Prefer functional updates when next state depends on previous:
    js
    setCount(c => c + 1);
    
  • useRef holds a stable object and does not trigger rerenders; useful to avoid re-creating closures around changing values.

Node.js#

  • Middleware factories (Express), curried config functions, and async flows rely heavily on closures.

Interview-Style Questions#

1.Define a closure.
A function together with its lexical environment, allowing it to remember variables from its defining scope.

2.Why does var + setTimeout in a loop log the same value? How to fix?
Because a single var binding is shared. Fix with let or an IIFE:

js
for (var i = 1; i <= 3; i++) {
  (i => setTimeout(() => console.log(i), 0))(i);
}

3.How do closures enable privacy?
By keeping variables in a scope that’s not directly exposed, only through returned functions.

4.Can closures cause memory leaks?
They can extend lifetimes. If you unintentionally retain references (e.g., listeners), memory can grow. Clean up listeners and avoid capturing heavy objects unnecessarily.


Quick Exercises#

1.Create a counter with reset

js
function counter(start = 0) {
  let n = start;
  return {
    next: () => ++n,
    reset: () => (n = 0)
  };
}

2.Build a simple rate limiter

js
function rateLimit(fn, ms) {
  let ready = true;
  return (...args) => {
    if (!ready) return;
    ready = false;
    fn(...args);
    setTimeout(() => (ready = true), ms);
  };
}

3.Once with error handling

js
function onceSafe(fn) {
  let called = false;
  return (...args) => {
    if (called) throw new Error("Already called");
    called = true;
    return fn(...args);
  };
}

Summary#

  • Closure = function + its lexical environment.
  • They power privacy, factories, async callbacks, and more.
  • Watch for loop scoping, stale closures, and unnecessary memory retention.

✨ Master closures, and a lot of JS “magic” becomes predictable.

Filed under#javascript#closures#scope#lexical environment

Was this worth your time?

One tap tells me what to write more of.

Mohsen Fallahnejad

Written by

Mohsen Fallahnejad

Frontend engineer explaining JavaScript, React & Next.js internals in posts you can finish over coffee. More about me

X ↗GitHub ↗LinkedIn ↗
← OlderJavaScript Promises ExplainedNewer →How useState Works Under the Hood (and Why It Needs use client)

Start the discussion

Questions, corrections, a better approach? I read and reply to every comment.

Read next

The $4.35 Item That Was Charged $4.34

Why 0.1 + 0.2 !== 0.3 in JavaScript, how floating-point math drops a cent when converting dollars to cents, and why money should be stored as integers.

Nov 3, 20265 min read

The Leaderboard Arrived Sorted. JavaScript Re-sorted It by User ID.

Why JavaScript object key order puts integer-like keys first in ascending order, how that silently reorders JSON from your API, and when to use an array or Map.

Nov 3, 20265 min read

The Settings Object That Came Back undefined

How automatic semicolon insertion (ASI) turns `return` followed by a newline into `return;`, and the lines where JavaScript refuses to add a semicolon at all.

Nov 2, 20265 min read

Get new posts in your inbox

No spam. Unsubscribe any time.

5 min left
Discuss👍 React
👍