JavaScript Closures Tutorial - Lexical Scope, Examples
JavaScript Closures
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.
The Short Definition
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.
function outer() {
const message = 'Hello from outer';
function inner() {
console.log(message);
}
return inner;
}
const savedFunction = outer();
savedFunction(); // Hello from outer
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.
Lexical Scope Comes First
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.
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();
The inner() function can access topic because it is written inside outer(). The call location does not create the scope. The code location does.
When Does a Closure Happen?
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.
Why Closures Matter
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.
Private State with Closures
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.
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
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.
Each Closure Gets Its Own State
Every time the outer function runs, JavaScript creates a new lexical environment. That means each closure can have its own independent state.
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
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 Factories
A function factory creates and returns another function. Closures make factories powerful because the returned function remembers the configuration passed to the factory.
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
double() remembers multiplier = 2, while triple() remembers multiplier = 3. This is the same outer function producing different behavior.
Closures in Event Handlers
Event handlers often use closures because setup code runs once, while the handler function runs later. The handler can remember variables from setup time.
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');
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.
Closures in Loops
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.
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
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.
Old Fix for var Loop Closures
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.
for (var i = 1; i <= 3; i++) {
(function(currentValue) {
setTimeout(function() {
console.log(currentValue);
}, 100);
})(i);
}
// Output:
// 1
// 2
// 3
The IIFE receives the current value of i as currentValue. Each timeout closes over a different function call's variable.
Closures with Timers
Timers show closures clearly because the callback runs after the outer function has completed.
function remindUser(name) {
const message = `Hi ${name}, take a short break.`;
setTimeout(function() {
console.log(message);
}, 2000);
}
remindUser('Asha');
The callback passed to setTimeout() remembers message, even though remindUser() is no longer running when the timer fires.
Debounce with Closures
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.
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
The returned function remembers timerId. Every call clears the previous timer and starts a new one.
Throttle with Closures
Throttle limits a function so it runs at most once during a time window. It is useful for scroll, mousemove, and resize events.
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);
The returned function remembers waiting. While waiting is true, extra calls are ignored.
Memoization with Closures
Memoization stores results so expensive work does not need to run again for the same input. A closure keeps the cache private.
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
The cache variable is not global. Only the returned function can access it.
Module Pattern with Closures
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 and Async Code
Closures are common in asynchronous JavaScript because callbacks, promises, and async functions often run after surrounding code has moved on.
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);
});
The returned loadUser() function remembers apiUrl. You do not need to pass the base URL every time.
Closure Does Not Copy Values
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.
function createLogger() {
let message = 'First message';
function log() {
console.log(message);
}
message = 'Updated message';
return log;
}
const logger = createLogger();
logger(); // Updated message
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.
Closures and Memory
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.
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();
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.
Closures vs Classes
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.
// 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;
}
}
Modern JavaScript classes support private fields with #, but closure-based privacy is still common and useful, especially for small utilities.
Interview Explanation
If you are asked "What is a closure?" in an interview, give a short definition and then explain with a small example.
function makeGreeting(name) {
return function() {
return `Hello, ${name}`;
};
}
const greetAsha = makeGreeting('Asha');
console.log(greetAsha()); // Hello, Asha
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."
Common Closure Use Cases
| 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 |
Common Beginner Mistakes
Summary
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.
- A closure is a function that remembers variables from its outer lexical scope.
- Closures happen because JavaScript uses lexical scope and functions can be passed around as values.
- Returned functions, callbacks, timers, and event handlers commonly create visible closures.
- Closures are useful for private state, factories, debounce, throttle, memoization, and module patterns.
-
letcreates a new loop binding for each iteration, whilevarshares one function-scoped binding. - Closures keep referenced variables alive, so be careful with long-lived handlers and large captured objects.
Thinking a closure stores a fixed copy of a variable value
A closure keeps access to the variable itself
Using var in async loop callbacks
Use let for a fresh binding per iteration
let makes loop closure behavior match what most beginners expect. Leaving event listeners alive forever
Remove listeners when they are no longer needed
Frequently Asked Questions
Level Up Your Javascript Skills
Master Javascript with these hand-picked resources