Devpuff
Pricing
Log InStart Free
  1. Home
  2. Blog
  3. JavaScript closures explained with 3 examples
Learn

JavaScript closures explained with 3 examples

A closure is a function that remembers the variables around it after the function that made them has finished. Three real uses and the classic loop bug.

July 21, 2026·10 Min read·By The Devpuff team
A function shown holding on to a variable from the outer function that created it, after that outer function has returned

A closure in JavaScript is a function that remembers the variables from where it was created, and can still read and change them later, even after the function that created those variables has finished running. Every function you write is a closure. Most of the time it does not matter, and then one day it does.

That "one day" is usually a bug: a counter that resets, a loop that prints the same number three times, a value that should have changed and did not. This post covers the three closures you will genuinely write, the loop bug that teaches the concept better than any definition, and the one cost nobody mentions.

Every output below was produced by running the code.

Example 1: a counter that keeps counting

The classic, and the shortest closure that does something you could not do otherwise:

function makeCounter() {
  let count = 0;

  return function () {
    count += 1;
    return count;
  };
}

const next = makeCounter();

console.log(next()); // 1
console.log(next()); // 2
console.log(next()); // 3

Look at what should be impossible here. makeCounter() finished running on the first line. Its local variable count should be gone. But calling next() keeps incrementing something, and that something is still there three calls later.

The inner function was created inside makeCounter, so it kept a live reference to count. Not a copy. The actual variable. As long as the inner function exists, so does count.

Each call makes a fresh one

This is the part that separates closures from global variables:

const next = makeCounter();
const other = makeCounter();

console.log(next()); // 1
console.log(next()); // 2
console.log(other()); // 1

other starts at 1, not 3. Every call to makeCounter() creates a new count, and each returned function closes over its own.

If count were a global, both would share it and the second counter would start at 3. Closures give you as many independent, private counters as you ask for, and no two can interfere with each other.

Example 2: private state, without a class

JavaScript has no private keyword on plain objects. Closures give you one anyway:

function createAccount(startingBalance) {
  let balance = startingBalance;

  return {
    deposit(amount) {
      balance += amount;
      return balance;
    },
    getBalance() {
      return balance;
    },
  };
}

const account = createAccount(100);
account.deposit(50);

console.log(account.getBalance()); // 150
console.log(account.balance); // undefined

account.balance is undefined because there is no such property. balance is a variable inside createAccount, not a field on the returned object. The only way to reach it is through the two functions that were created alongside it, and both were written by you.

Nothing outside can set the balance to a million. Not a typo, not another module, not a well-meaning teammate. That guarantee is worth more than it looks, because it means when the balance is wrong you have exactly two places to look.

This shape has a name, the module pattern, and it predates JavaScript classes by about a decade. It is still the simplest way to make something genuinely tamper-proof.

Example 3: run once, remember the answer

A closure is the neatest way to make something happen exactly once:

function once(fn) {
  let called = false;
  let result;

  return function (...args) {
    if (called) return result;
    called = true;
    result = fn(...args);
    return result;
  };
}

const connect = once((url) => {
  console.log("connecting to", url);
  return { url, id: 1 };
});

console.log(connect("wss://devpuff.com"));
console.log(connect("wss://devpuff.com"));

Output:

connecting to wss://devpuff.com
{ url: 'wss://devpuff.com', id: 1 }
{ url: 'wss://devpuff.com', id: 1 }

"connecting to" appears once. The second call skipped the work entirely and handed back the stored result.

Two variables, called and result, live in the closure. They are the function's memory between calls. Swap the body and you have a cache. Keep the shape and add a timer and you have debounce, the thing that stops a search box firing a request per keystroke:

function debounce(fn, wait) {
  let timer;

  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), wait);
  };
}

const search = debounce((q) => console.log("searching for", q), 50);

search("j");
search("ja");
search("jav");
search("java");

Output:

searching for java

Four calls, one search. timer lives in the closure, so each call can cancel the timer the previous call set. Without a closure you would need a variable somewhere outside, and then two debounced functions would fight over it.

The mechanism has not changed once across three examples. A variable in an outer scope, a function that keeps using it.

The bug that teaches closures faster than any definition

Run this and predict the output before reading on:

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

It prints:

4
4
4

Not 1 2 3. And not because setTimeout is broken.

var is function-scoped, so the entire loop shares one i. All three arrow functions close over that same variable. By the time the timers fire, the loop has finished, and finishing means i reached 4 and failed the i <= 3 check.

Three functions, one variable, and the value they see is whatever it is when they run.

The fix is one word:

for (let i = 1; i <= 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// 1
// 2
// 3

let is block-scoped, and a for loop creates a fresh binding each iteration. Now there are three separate i variables, and each function closes over its own.

That is the entire difference between var and let in one example, and it is why modern JavaScript does not use var. If this bug is new to you, run both versions. Watching 4 4 4 appear does something a paragraph cannot.

Closures remember variables, not values

The loop bug generalises into the single most useful sentence about closures: a closure captures the variable, not a snapshot of what was in it.

function makePair() {
  let n = 0;
  return [() => (n += 1), () => n];
}

const [inc, read] = makePair();
inc();
inc();

console.log(read()); // 2

Two different functions, one shared n. inc changes it, read sees the change, because they closed over the same variable rather than over the number 0.

Keep that sentence and the loop bug together and closures stop being mysterious. Everything else follows from them.

What closures cost

Here is the part most explanations leave out. A closure keeps its variables alive, and "alive" means "in memory".

function attach() {
  const bigList = new Array(1_000_000).fill("data");

  return function () {
    return bigList.length;
  };
}

const getSize = attach();

getSize is a tiny function that returns one number. But it closed over bigList, so that million-item array cannot be cleaned up while getSize exists. Store getSize somewhere long-lived and you have a leak that no line of code obviously causes.

The fix is to close over only what you need:

function attach() {
  const bigList = new Array(1_000_000).fill("data");
  const size = bigList.length;

  return function () {
    return size;
  };
}

Now the closure holds one number and the array can go.

This is rarely worth thinking about for small values. It is worth thinking about for anything holding a DOM node, a large response, or an event listener you never remove. Long-lived listeners are the most common real-world version of this problem.

You are already using closures

Once you can see them, they are everywhere:

const langs = ["html", "css", "php"];
const target = "php";

langs.filter((l) => l !== target); // closes over target

That arrow function reads target, which lives outside it. Closure.

So does every event handler that references a variable from the surrounding function. So does every callback passed to setTimeout. So does every React hook that reads a prop or a piece of state, which is exactly why useEffect has a dependency array. The array exists to tell React which closed-over values the effect actually reads, so it knows when the old closure is stale.

That connection is worth holding on to. React's most confusing hook is a closure problem wearing a framework hat.

Event handlers are the other place this shows up in plain JavaScript, and the symptom is a button that keeps using an old value:

function setup() {
  let mode = "light";

  document.querySelector("#toggle").addEventListener("click", () => {
    console.log("mode is", mode); // always the current mode, not the first one
  });

  mode = "dark";
}

The handler logs dark, not light, even though mode was light when the listener was attached. Closures capture the variable, so the handler sees whatever is in it at click time.

That is usually what you want, and it is why the same code with const mode in a loop behaves so differently from var. When a handler surprises you, ask which variable it closed over and when that variable last changed. The answer is nearly always there.

Closures or a class?

JavaScript now has private class fields, which cover the same ground as example 2:

class Counter {
  #count = 0;

  next() {
    return (this.#count += 1);
  }
}

const c = new Counter();
c.next();

console.log(c.next()); // 2
console.log(c.count); // undefined

Same privacy, same result. So which?

Closure Class
Privacy By construction, nothing to bypass #field, enforced by the language
this Not involved Involved, and it can be rebound
Detached methods Keep working Lose this unless bound
Many instances One function call each new, and prototypes shared
Reads as A function that returns something A blueprint

The practical tiebreaker is that last-but-one row. Pass a class method somewhere as a callback and this goes missing, which is why onClick={this.handleClick} needs binding and onClick={handleClick} from a closure does not. That single difference is a large part of why React moved from classes to functions and hooks.

Use a class when you want many instances of a thing with a name and a shape. Use a closure when you want one function that remembers something.

How to spot a closure in your own code

Two questions:

  1. Is there a function defined inside another function, or inside a block?
  2. Does the inner one use a variable it did not declare itself?

Both yes means closure. The variable stays alive as long as the inner function does.

That is all a closure is. Not a feature you switch on, not a pattern you adopt. Just what happens when a function is written somewhere, and JavaScript declines to forget where.

Which is why "every function is a closure" is technically true and not very useful. The word only earns its keep when the outer function has already returned and the inner one is still going, because that is the case where the behaviour stops matching intuition. MDN's closures guide is the reference to keep open.

Practice makes this click

Closures are the concept most people nod along to and then fail an interview question on, because reading 4 4 4 is not the same as having produced it yourself and then fixed it.

Intermediate JavaScript works through scope, closures, and higher-order functions with the code running in your browser, so you can change let to var and watch the output change. Related reading: How to Remove an Element from an Array, which uses closures on every line without saying so, and the JavaScript hub for what to learn next.

Keep reading

More from the blog

Two database tables side by side with lines connecting matching rows, and the joined result set below them
July 21, 2026·9 min readLearn

SQL joins explained, which rows survive and why

INNER, LEFT, RIGHT, FULL and CROSS joins on the same two tables, with the real result rows. Plus why the Venn diagram everyone shows you is misleading.

Read more
A Python for loop printing an index next to each item in a list, with enumerate supplying both values
July 21, 2026·11 min readLearn

Python for loop with index, enumerate() explained

Use enumerate() to get the index and the item in one loop. The syntax, the start argument, and why range(len()) breaks on half of what you loop over.

Read more
{ }
✦

Ready to write some code?

Put this into practice - start your first free lesson. No setup, no credit card.

Start learning free
Devpuff

Learn to code by doing. One tiny, playful lesson at a time.

Learn
ProgramsCoursesPricing
Company
AboutBlogResources
Support
Help CenterContactStatus
Programs
Frontend Development
Courses
HTML Basics
Learn to Code
Learn JavaScriptLearn SQLLearn HTMLLearn CSSLearn ReactBrowse All Topics
Platform Comparisons
Devpuff vs CodecademyDevpuff vs MimoDevpuff vs SololearnDevpuff vs freeCodeCampRead All Comparisons
© 2026 Devpuff. All rights reserved.Privacy PolicyTerms and ConditionsCookies PolicyRefund Policy