closures is an important JavaScript topic because it shows up in real projects, debugging sessions, and interviews. Learn the meaning first, then connect it to a small working example so the rule does not stay abstract.
Focus on what problem closures solves, where developers usually make mistakes, and how to verify the result with output, behavior, or a small test.
A strong understanding of closures should include syntax, behavior, one realistic use case, one failure case, and one quick way to check your work.
JavaScript Closures Lexical Scope should be studied as a practical JavaScript lesson, not as a label. Start by naming the input, the rule that changes the input, and the result a learner should be able to predict after reading the page.
In the javascript > closures page, the notes should connect the definition with a working scenario, a mistake that beginners actually make, and the exact check that proves the fix. That makes the topic useful for coding, debugging, and interview revision.
A closure is created when a function remembers variables from the scope where it was created, even after that outer scope has finished running. In simple words, an inner function can keep access to variables from its parent function.
Closures are not a special syntax. They are a natural result of two JavaScript features working together: lexical scope and functions as values. Because functions can be returned, stored in variables, passed as callbacks, and executed later, they may carry their surrounding scope with them.
A closure is a function bundled together with references to its outer lexical environment. That means the function can access variables that were in scope when the function was created.
When outer() finishes, its local execution is done. But inner() still needs message, so JavaScript keeps the required variable alive. That remembered access is the closure.
function outer() {
const message = 'Hello from outer';
function inner() {
console.log(message);
}
return inner;
}
const savedFunction = outer();
savedFunction(); // Hello from outer
To understand closures, first understand lexical scope. Lexical scope means a variable's scope is decided by where the variable and function are written in the source code, not by where the function is called.
The inner() function can access topic because it is written inside outer(). The call location does not create the scope. The code location does.
const globalName = 'Tutorials Logic';
function outer() {
const topic = 'Closures';
function inner() {
console.log(globalName); // available from global scope
console.log(topic); // available from outer scope
}
inner();
}
outer();
Every function has access to its outer scope while it runs. A closure becomes especially visible when a function continues to use that outer scope later. This usually happens when a function is returned, assigned to a variable, passed as a callback, or used by a timer or event listener.
Closures are everywhere in JavaScript. They power private state, function factories, callbacks, event handlers, debounce and throttle utilities, memoization, module patterns, and many framework patterns.
One of the most useful closure patterns is private state. Variables inside a function are not directly accessible from outside, but returned functions can still read and update them.
The count variable is private. Code outside createCounter() cannot read it directly, but the returned methods can access it because they close over the same lexical environment.
function createCounter() {
let count = 0;
return {
increment() {
count += 1;
return count;
},
decrement() {
count -= 1;
return count;
},
getValue() {
return count;
}
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getValue()); // 2
console.log(counter.count); // undefined
Every time the outer function runs, JavaScript creates a new lexical environment. That means each closure can have its own independent state.
first and second were created by the same function, but they do not share the same count. Each call to createCounter() creates a fresh private variable.
function createCounter(start = 0) {
let count = start;
return function increment() {
count += 1;
return count;
};
}
const first = createCounter(0);
const second = createCounter(10);
console.log(first()); // 1
console.log(first()); // 2
console.log(second()); // 11
console.log(second()); // 12
A function factory creates and returns another function. Closures make factories powerful because the returned function remembers the configuration passed to the factory.
double() remembers multiplier = 2, while triple() remembers multiplier = 3. This is the same outer function producing different behavior.
function multiplyBy(multiplier) {
return function(number) {
return number * multiplier;
};
}
const double = multiplyBy(2);
const triple = multiplyBy(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
Event handlers often use closures because setup code runs once, while the handler function runs later. The handler can remember variables from setup time.
After setupButton() finishes, the click handler still remembers clicks and label. That is why every button can keep its own click count without using global variables.
function setupButton(button, label) {
let clicks = 0;
button.addEventListener('click', function() {
clicks += 1;
console.log(`${label} clicked ${clicks} times`);
});
}
const saveButton = document.querySelector('#save');
setupButton(saveButton, 'Save');
Closures inside loops are a common source of confusion. With var, callbacks share one function-scoped variable. With let, each loop iteration gets a new block-scoped binding.
The var callbacks all see the same i, which has become 4 after the loop finishes. The let callbacks each close over a different j binding.
for (var i = 1; i <= 3; i++) {
setTimeout(function() {
console.log('var:', i);
}, 100);
}
for (let j = 1; j <= 3; j++) {
setTimeout(function() {
console.log('let:', j);
}, 100);
}
// Output:
// var: 4
// var: 4
// var: 4
// let: 1
// let: 2
// let: 3
Before let, developers often used an IIFE to create a new scope for each loop iteration. You will still see this pattern in old codebases and interview questions.
The IIFE receives the current value of i as currentValue. Each timeout closes over a different function call's variable.
for (var i = 1; i <= 3; i++) {
(function(currentValue) {
setTimeout(function() {
console.log(currentValue);
}, 100);
})(i);
}
// Output:
// 1
// 2
// 3
Timers show closures clearly because the callback runs after the outer function has completed.
The callback passed to setTimeout() remembers message, even though remindUser() is no longer running when the timer fires.
function remindUser(name) {
const message = `Hi ${name}, take a short break.`;
setTimeout(function() {
console.log(message);
}, 2000);
}
remindUser('Asha');
Debounce waits until repeated calls stop for a certain time. It is useful for search boxes, resize handlers, and input validation. Closures keep track of the timer ID between calls.
The returned function remembers timerId. Every call clears the previous timer and starts a new one.
function debounce(callback, delay) {
let timerId;
return function(...args) {
clearTimeout(timerId);
timerId = setTimeout(() => {
callback.apply(this, args);
}, delay);
};
}
const search = debounce(function(query) {
console.log('Searching for:', query);
}, 300);
search('j');
search('ja');
search('jav');
search('java'); // only this call runs after 300ms
Throttle limits a function so it runs at most once during a time window. It is useful for scroll, mousemove, and resize events.
The returned function remembers waiting. While waiting is true, extra calls are ignored.
function throttle(callback, delay) {
let waiting = false;
return function(...args) {
if (waiting) return;
callback.apply(this, args);
waiting = true;
setTimeout(() => {
waiting = false;
}, delay);
};
}
const onScroll = throttle(function() {
console.log('Scroll handler ran');
}, 500);
window.addEventListener('scroll', onScroll);
Memoization stores results so expensive work does not need to run again for the same input. A closure keeps the cache private.
The cache variable is not global. Only the returned function can access it.
function memoize(fn) {
const cache = new Map();
return function(value) {
if (cache.has(value)) {
console.log('From cache');
return cache.get(value);
}
console.log('Calculated');
const result = fn(value);
cache.set(value, result);
return result;
};
}
const square = memoize(n => n * n);
console.log(square(5)); // Calculated, 25
console.log(square(5)); // From cache, 25
Before ES modules became common, developers used closures to create modules with private variables and a public API. This pattern is still useful for understanding older JavaScript code.
const cartModule = (function() {
const items = [];
return {
addItem(name, price) {
items.push({ name, price });
},
getTotal() {
return items.reduce((total, item) => total + item.price, 0);
},
getCount() {
return items.length;
}
};
})();
cartModule.addItem('Keyboard', 50);
cartModule.addItem('Mouse', 20);
console.log(cartModule.getCount()); // 2
console.log(cartModule.getTotal()); // 70
console.log(cartModule.items); // undefined
Closures are common in asynchronous JavaScript because callbacks, promises, and async functions often run after surrounding code has moved on.
The returned loadUser() function remembers apiUrl. You do not need to pass the base URL every time.
function createUserLoader(apiUrl) {
return async function loadUser(id) {
const response = await fetch(`${apiUrl}/users/${id}`);
return response.json();
};
}
const loadUser = createUserLoader('https://api.example.com');
// loadUser remembers apiUrl.
loadUser(42).then(user => {
console.log(user);
});
A common misconception is that closures copy variable values. In reality, closures keep access to variables. If the variable changes, the closure sees the latest value.
log() does not store a frozen copy of 'First message'. It keeps access to the variable message, which was updated before the function was returned.
function createLogger() {
let message = 'First message';
function log() {
console.log(message);
}
message = 'Updated message';
return log;
}
const logger = createLogger();
logger(); // Updated message
Closures keep variables alive as long as the closure is reachable. This is useful, but it also means large objects can stay in memory if a closure still references them. In most apps this is not a problem, but it matters for long-lived event listeners, caches, and large data structures.
Remove long-lived event listeners when they are no longer needed, especially in single-page applications where pages and components can mount and unmount without a full reload.
function attachHandler(button, largeData) {
function handleClick() {
console.log('Clicked');
// If largeData is referenced here, it stays alive.
}
button.addEventListener('click', handleClick);
return function cleanup() {
button.removeEventListener('click', handleClick);
};
}
const cleanup = attachHandler(document.querySelector('button'), []);
// Later, when the handler is no longer needed:
cleanup();
Closures and classes can both manage state. Closures are great for small private pieces of state and factory-style APIs. Classes are useful when you need many related methods, inheritance, or a familiar object-oriented structure.
Modern JavaScript classes support private fields with #, but closure-based privacy is still common and useful, especially for small utilities.
// Closure style
function createCounter() {
let count = 0;
return {
increment: () => ++count,
value: () => count
};
}
// Class style
class Counter {
#count = 0;
increment() {
return ++this.#count;
}
value() {
return this.#count;
}
}
If you are asked "What is a closure?" in an interview, give a short definition and then explain with a small example.
A strong interview answer: "A closure is when a function keeps access to variables from its outer lexical scope even after that outer function has returned. In this example, the returned function still remembers name."
function makeGreeting(name) {
return function() {
return `Hello, ${name}`;
};
}
const greetAsha = makeGreeting('Asha');
console.log(greetAsha()); // Hello, Asha
| Use case | What the closure remembers |
|---|---|
| Private counter | The current count value |
| Function factory | Configuration such as multiplier, prefix, or API URL |
| Event handler | Setup state, DOM references, labels, or counters |
| Debounce | The current timer ID |
| Throttle | Whether the function is waiting |
| Memoization | A private cache |
Closures happen when a function keeps access to variables from its outer lexical scope. They let JavaScript functions remember state, create private variables, produce customized functions, and support callbacks that run later. Once you understand lexical scope and function values, closures become less mysterious: a closure is simply a function carrying the variables it still needs.
class JavaScriptClosuresLexicalScopeReview {
public static void main(String[] args) {
String state = "ready";
System.out.println("JavaScript Closures Lexical Scope: " + state);
}
}
String value = null;
if (value == null) {
System.out.println("JavaScript Closures Lexical Scope: handle the missing value before continuing");
}
Calling a value before checking whether it actually holds a function reference.
Trace the variable assignment, the property lookup, and the actual call expression.
Memorizing JavaScript Closures Lexical Scope without the situation where it is useful.
Connect JavaScript Closures Lexical Scope to a concrete JavaScript task.
Testing JavaScript Closures Lexical Scope only with the perfect input.
Include empty, missing, duplicate, incompatible, or failed cases when relevant.
Memorizing JavaScript Closures Lexical Scope without the situation where it is useful.
Connect JavaScript Closures Lexical Scope to a concrete JavaScript task.
A closure is a function that keeps access to variables from its outer lexical scope, even after the outer function has finished running.
No. Returning a function is the easiest example, but closures also appear in callbacks, event listeners, timers, promises, and any function that uses outer-scope variables later.
No. Closures keep access to variables, not frozen copies of values. If the variable changes, the closure can observe the changed value.
Closures reveal whether you understand lexical scope, async callbacks, private state, and common JavaScript patterns such as debounce, memoization, and function factories.
Explore 500+ free tutorials across 20+ languages and frameworks.