JavaScript Closures: How They Work and Why They Matter
Closures are one of the most important ideas in JavaScript functions. They let a function remember variables from the scope where it was created, even after that outer scope has finished running.
Quick answer: A closure is created when an inner function keeps access to variables from its outer function. This is why JavaScript functions can “remember” state between calls, build private data, and power many common patterns.
Difficulty: Beginner to Intermediate
You'll understand this better if you know: how functions work in JavaScript, what scope means, and the difference between local and global variables.
1. What Is Closures?
A closure is a function plus the variables it can still access from the place where it was created. In JavaScript, every function is created with a scope chain, and an inner function can keep access to the outer function’s variables even after the outer function returns.
- Closures are based on lexical scoping, which means scope is determined by where code is written.
- They allow functions to retain access to outer variables.
- They are created naturally whenever a function uses variables from an outer scope.
- They are not a special syntax; they are a behavior of functions.
2. Why Closures Matter
Closures make JavaScript practical for real applications. They help you write functions that keep private state, create reusable function factories, handle events, and remember data between calls without using global variables.
Without closures, many common JavaScript patterns would be much harder to write cleanly. They are also behind a lot of callback-based code, timers, and module patterns.
3. Basic Syntax or Core Idea
The smallest closure example
This example shows a function returning another function. The inner function can still use name after the outer function has finished.
function createGreeter(name) {
return function () {
return `Hello, ${name}!`;
};
}
const greetAlice = createGreeter("Alice");
console.log(greetAlice());The inner function forms a closure over name. Even though createGreeter has already returned, the returned function still remembers the value.
What makes it a closure?
Two things happen together:
- An inner function is defined inside an outer function.
- The inner function uses a variable from the outer function.
That is enough to create a closure in JavaScript.
4. Step-by-Step Examples
Example 1: Remembering a value
This example returns a counter function that remembers its own private state.
function makeCounter() {
let count = 0;
return function () {
count += 1;
return count;
};
}
const counterA = makeCounter();
console.log(counterA()); // 1
console.log(counterA()); // 2Each call to counterA still sees the same count variable. That is the closure keeping the state alive.
Example 2: Independent instances
Each call to the outer function creates a new closure with its own separate data.
function createScoreTracker() {
let score = 0;
return {
addPoint() {
score += 1;
return score;
},
getScore() {
return score;
}
};
}
const playerOne = createScoreTracker();
const playerTwo = createScoreTracker();
playerOne.addPoint();
playerOne.addPoint();
playerTwo.addPoint();
console.log(playerOne.getScore()); // 2
console.log(playerTwo.getScore()); // 1Each object has methods that share the same private score, but the two trackers do not interfere with each other.
Example 3: Using closures in loops
Closures are often used when a callback needs to remember a value from a loop.
function buildHandlers() {
const handlers = [];
for (let i = 1; i <= 3; i++) {
handlers.push(function () {
return i * 2;
});
}
return handlers;
}
const handlers = buildHandlers();
console.log(handlers[0]()); // 2
console.log(handlers[1]()); // 4
console.log(handlers[2]()); // 6Because let creates a new binding for each loop iteration, each function remembers the value it was created with.
Example 4: A closure used for validation
Closures can also enforce rules by keeping configuration hidden inside a function.
function createLengthValidator(minLength) {
return function (value) {
return value.length >= minLength;
};
}
const isValidUsername = createLengthValidator(8);
console.log(isValidUsername("alex1234")); // true
console.log(isValidUsername("sam")); // falseThe validator remembers minLength without exposing it to the rest of the program.
5. Practical Use Cases
- Creating private data that should not be changed directly from outside a function.
- Building factory functions that generate customized behavior.
- Writing event handlers that need to remember setup data.
- Using timers or asynchronous callbacks that refer to earlier values.
- Adding helper functions inside modules without leaking variables globally.
6. Common Mistakes
Mistake 1: Expecting closures to copy values instead of keeping references
A closure does not freeze a variable’s value at the moment the function is created. It keeps access to the variable itself, so later changes are visible.
Problem: This code expects the returned function to always print the original value, but the outer variable changes before the function runs.
function makeLogger() {
let message = "Start";
return function () {
console.log(message);
};
}
const log = makeLogger();
message = "Changed";Fix: Keep the change inside the outer function or pass the value as an argument instead of trying to assign to an out-of-scope variable.
function makeLogger() {
let message = "Start";
return function () {
console.log(message);
};
}
const log = makeLogger();
log();The corrected version works because the closure reads the variable that belongs to its own lexical environment.
Mistake 2: Reusing a loop variable in asynchronous callbacks
This is a classic closure bug when code uses var in a loop and schedules callbacks that run later.
Problem: All callbacks see the same final value, so the output is not what beginners expect.
for (var i = 1; i <= 3; i++) {
setTimeout(function () {
console.log(i);
}, 0);
}Fix: Use let so each iteration gets its own binding.
for (let i = 1; i <= 3; i++) {
setTimeout(function () {
console.log(i);
}, 0);
}The fixed version works because each callback closes over a different i.
Mistake 3: Confusing closure scope with global variables
If a variable is not declared in the right place, JavaScript looks for it in the outer scopes. That can lead to accidental global behavior or a ReferenceError.
Problem: The function tries to use count without declaring it in a scope that all related functions can share.
function createBrokenCounter() {
return function () {
count += 1;
return count;
};
}
const brokenCounter = createBrokenCounter();
console.log(brokenCounter());Fix: Declare the variable in the outer function so the inner function closes over it.
function createCounter() {
let count = 0;
return function () {
count += 1;
return count;
};
}This works because both functions share the same lexical environment, and the variable exists for the closure to use.
7. Best Practices
Practice 1: Use closures for private state, not for everything
Closures are great for hiding data, but they are not always the simplest solution. If data needs to be shared broadly, a plain object or class may be easier to read.
function createBankAccount() {
let balance = 0;
return {
deposit(amount) {
balance += amount;
return balance;
},
getBalance() {
return balance;
}
};
}Use this style when the state should stay private and only a small API should be exposed.
Practice 2: Keep closed-over data small
Closures can keep objects alive longer than expected. If you close over a large structure that is no longer needed, memory may stay in use until the closure is released.
function makeHandler(userId) {
return function () {
return `User: ${userId}`;
};
}This is better than capturing an entire object when you only need one small property.
Practice 3: Name factory functions clearly
Functions that return closures are easier to maintain when their names describe what they create.
function createPrefixer(prefix) {
return function (text) {
return `${prefix}: ${text}`;
};
}Clear names make it obvious that the outer function configures and returns a new function.
8. Limitations and Edge Cases
- Closures keep variables alive, so they can extend object lifetimes and increase memory use if they capture too much data.
- They capture variables by reference to the binding, not by making a snapshot of the value.
- Each call to an outer function creates a new closure environment, which is useful but can add overhead in large loops if overused carelessly.
- Browser event handlers and timers often reveal closure behavior later than expected, which can look like a bug if you expect immediate execution.
- Some closure-related bugs appear only with asynchronous code, so the output can look correct in the source but wrong at runtime.
9. Practical Mini Project
Let’s build a small task tracker using closures to keep the task list private. The returned methods can add tasks, list tasks, and remove tasks without exposing the internal array directly.
function createTaskList() {
let tasks = [];
return {
add(task) {
tasks.push(task);
},
remove(task) {
tasks = tasks.filter(function (item) {
return item !== task;
});
},
all() {
return [...tasks];
}
};
}
const todo = createTaskList();
todo.add("Write docs");
todo.add("Review code");
todo.remove("Write docs");
console.log(todo.all()); // ["Review code"]This mini project uses a closure to keep tasks private while still offering useful operations through methods.
10. Key Points
- A closure is a function that remembers variables from its outer scope.
- Closures are created by lexical scoping, not by any special keyword.
- They are useful for private state, callbacks, and factory functions.
- Common bugs come from loop variables, asynchronous code, and misunderstanding references.
- Use closures when they make the code clearer and more focused.
11. Practice Exercise
Build a function that returns a counter with three methods:
- increment() increases the count by 1.
- decrement() decreases the count by 1.
- value() returns the current count.
Expected output:
- Starting value is 0.
- After two increments and one decrement, the value should be 1.
Hint: store the count in a variable inside the outer function, and return an object whose methods share that variable.
Solution:
function createCounter() {
let count = 0;
return {
increment() {
count += 1;
},
decrement() {
count -= 1;
},
value() {
return count;
}
};
}
const counter = createCounter();
counter.increment();
counter.increment();
counter.decrement();
console.log(counter.value()); // 112. Final Summary
Closures are a core part of JavaScript’s function model. They let inner functions keep access to outer variables, which makes it possible to preserve state, hide data, and build reusable function factories. Once you understand closures, many common JavaScript patterns become easier to read and write.
They are especially useful when you need private state without classes, or when a callback needs to remember values from an earlier step. At the same time, closures can create confusing bugs if you accidentally reuse loop variables, rely on the wrong scope, or keep more data alive than necessary.
If you want to go further, study lexical scoping, function expressions, and how closures behave in asynchronous code. Those topics will make closure-based patterns much easier to reason about in real projects.