JavaScript Closures Explained: From Basics to Tricky Interview Questions
JavaScript has several concepts that seem confusing when you first encounter them but become incredibly powerful once you understand how they work internally. Closures are one of those concepts.
If you’ve ever wondered how a function can access variables from its outer scope even after the outer function has finished executing, you’re about to discover the answer.
Closures are not just an interview topic. They are used in real-world applications, React hooks, event handlers, callbacks, data privacy, memoization, and many other JavaScript patterns. In this article, we’ll cover:
- What closures are and how they work.
- Lexical scope and the scope chain.
- Practical examples of closures.
- Common mistakes developers make.
- Frequently asked JavaScript interview questions with solutions.
Let’s start with the basics.
1. What Is a Closure in JavaScript?
A closure is created when a function retains access to variables from its surrounding lexical scope, even when that function is executed outside that scope. That definition might sound complicated, so let’s understand it with a simple example.
function outer() {
const message = "Hello, JavaScript!";
function inner() {
console.log(message);
}
return inner;
}
const myFunction = outer();
myFunction();Output:
Hello, JavaScript!Let’s understand what happened.
- We called
outer(). - Inside
outer(), we created a variable namedmessage. - We defined an inner function named
inner(). - Instead of executing
inner(), we returned it. - The
outer()function finished executing. - We called
myFunction(), which references the returnedinnerfunction. - Surprisingly,
inner()could still accessmessage.
Why? Because the returned function retains access to its lexical environment, including the variables it needs from the surrounding scope. That’s a closure. The important point is that the outer function does not need to remain actively executing for the inner function to access its captured variables.
2. Understanding Lexical Scope
Before diving deeper into closures, we need to understand lexical scope. Lexical scope means that variable access is determined by where functions and blocks are defined in the source code, rather than where those functions are called. Consider this example:
const name = "Anshul";
function outer() {
const greeting = "Hello";
function inner() {
console.log(greeting, name);
}
inner();
}
outer();Output:
Hello AnshulThe inner() function can access:
greeting, declared in its parent function.name, declared in the global scope.
JavaScript resolves variables by looking through the surrounding lexical environments. This is commonly described as the scope chain.
Now consider what happens when we return inner() from outer() instead of calling it immediately. The inner function still retains access to the lexical environment in which it was defined. That relationship is the foundation of closures.
3. A Practical Example: Creating a Counter
Let’s move from theory to something useful. Suppose we need a counter that can be incremented but whose internal value cannot be directly modified from outside. Here’s one way to implement it using a closure:
function createCounter() {
let count = 0;
return function () {
count++;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3Every time we call counter(), the value increases. But where is count stored? It’s inside createCounter(). Although createCounter() has finished executing, the returned function retains access to count. Let’s create another counter:
const counter1 = createCounter();
const counter2 = createCounter();
console.log(counter1()); // 1
console.log(counter1()); // 2
console.log(counter2()); // 1
console.log(counter2()); // 2Notice that the two counters maintain independent state. Each invocation of createCounter() creates a new lexical environment with its own count variable. This makes closures useful for creating independent pieces of state without exposing those variables directly.
4. Why Are Closures Useful in Real-World Applications?
Closures aren’t just a clever JavaScript trick. They solve practical problems.
A. Data Privacy
JavaScript doesn’t require classes to implement private state. Closures can provide a simple way to hide internal variables.
function createBankAccount(initialBalance) {
let balance = initialBalance;
return {
deposit(amount) {
balance += amount;
return balance;
},
getBalance() {
return balance;
}
};
}
const account = createBankAccount(1000);
account.deposit(500);
console.log(account.getBalance()); // 1500
console.log(account.balance); // undefinedThe balance variable isn't exposed as a property of account. Only the methods returned by createBankAccount() can access it. This is a useful example of encapsulation through closures.
In production applications, financial operations would also require validation and appropriate security controls.
B. Event Handlers
Closures are common in event-driven programming.
function attachClickHandler(button, userName) {
button.addEventListener("click", function () {
console.log(`Hello, ${userName}!`);
});
}The callback retains access to userName, even though attachClickHandler() has already returned by the time the user clicks the button. This pattern is useful in browser applications, UI components, and callbacks.
C. Function Factories
Closures let us create specialized functions.
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)); // 15Instead of writing separate functions for doubling and tripling, we create a reusable function factory.
D. Memoization
Closures can also help preserve cached results between function calls.
function memoizeSquare() {
const cache = new Map();
return function (number) {
if (cache.has(number)) {
console.log("Returning cached result");
return cache.get(number);
}
const result = number * number;
cache.set(number, result);
return result;
};
}
const square = memoizeSquare();
console.log(square(5)); // 25
console.log(square(5)); // 25The cache variable remains accessible to the returned function. The second call can reuse the previously calculated result. This example illustrates the concept; real memoization utilities often need more sophisticated cache keys and eviction policies.
5. JavaScript Closure Interview Questions
Now let’s explore the questions that frequently appear in JavaScript interviews. These questions test more than your ability to define a closure. They test your understanding of scope, execution, variable bindings, and asynchronous behavior.
Question 1: What will this code print?
function outer() {
let count = 10;
return function () {
console.log(count);
};
}
const fn = outer();
count = 20;
fn();Answer:
10Why? The count variable declared inside outer() is a local variable. The assignment count = 20 creates or modifies a different binding in the surrounding scope, depending on the execution environment. It doesn’t change the local count captured by the returned function.
Interview takeaway: Closures retain access to their surrounding lexical bindings, not unrelated variables that happen to have the same name.
Question 2: What is the output?
function createCounter() {
let count = 0;
return function () {
count++;
console.log(count);
};
}
const a = createCounter();
const b = createCounter();
a();
a();
b();
a();
b();Answer:
1
2
1
3
2Each call to createCounter() creates a separate count variable. The a function closes over one variable, while b closes over another.
Get Anshul Gupta’s stories in your inbox
Join Medium for free to get updates from this writer.
Interview takeaway: Closures preserve access to their own lexical environments, so different function instances can maintain independent state.
Question 3: The Famous var Loop Problem
This is one of the most common JavaScript closure interview questions. What will this code print?
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}Answer:
3
3
3Why doesn’t it print 0, 1, and 2? Because var is function-scoped, not block-scoped. All three callbacks close over the same i binding. The loop finishes before the callbacks execute, and by that time, i has reached 3. When the callbacks run, they all read the current value of the same variable.
Solution 1: Use let
for (let i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}Output:
0
1
2With let in a loop, JavaScript creates a separate per-iteration binding for the loop variable, so each callback captures the corresponding value.
Solution 2: Use an IIFE
for (var i = 0; i < 3; i++) {
(function (value) {
setTimeout(() => {
console.log(value);
}, 1000);
})(i);
}Output:
0
1
2The immediately invoked function expression (IIFE) receives the current value of i as an argument. Each invocation creates a separate parameter binding named value.
Interview takeaway: Understand the difference between var and let, and how closures interact with variable bindings in asynchronous callbacks.
Question 4: Does a Closure Capture a Value or a Variable?
Consider this example:
function outer() {
let count = 0;
const increment = () => {
count++;
};
const print = () => {
console.log(count);
};
increment();
increment();
print();
}
outer();Output:
2The closure doesn’t simply take a frozen snapshot of the value 0. Instead, it retains access to the lexical binding for count. When that binding changes, subsequent reads observe the updated value. This is why both increment() and print() can work with the same variable.
Interview takeaway: A closure retains access to lexical bindings, not necessarily a copy of the values those bindings held when the function was created.
Question 5: Can a Closure Access Variables After the Outer Function Returns?
function outer() {
const message = "Closures are powerful";
return function inner() {
return message;
};
}
const fn = outer();
console.log(fn());Answer:
Closures are powerfulYes. The returned function retains access to the lexical environment containing message. JavaScript manages the lifetime of the necessary environment so that it remains available while reachable closures need it. This doesn’t mean the entire outer function keeps executing in the background.
Interview takeaway: A function can outlive the execution of the scope in which it was defined.
Question 6: What Is the Output When We Modify an Object?
function outer() {
const user = {
name: "Anshul"
};
return function () {
console.log(user.name);
};
}
const fn = outer();
fn();Now suppose we modify the object:
// After the previous code:
// If we still have a reference to the object:
user.name = "Rahul";This last assignment won’t work from outside outer() because user isn't available in that scope. A better demonstration is:
function outer() {
const user = {
name: "Anshul"
};
return {
printName() {
console.log(user.name);
},
rename(newName) {
user.name = newName;
}
};
}
const result = outer();
result.printName(); // Anshul
result.rename("Rahul");
result.printName(); // RahulAnswer:
Anshul
RahulThe closure retains access to the same object reference. Mutating that object changes the property observed by the other method. Also notice that const prevents reassignment of the user binding; it does not make the object immutable.
Interview takeaway: Closures preserve access to bindings, and objects referenced by those bindings can still be mutated.
Question 7: What Happens When Closures Are Used with setTimeout?
function greet(name) {
setTimeout(() => {
console.log(`Hello, ${name}`);
}, 1000);
}
greet("Anshul");Answer:
Hello, AnshulThe callback passed to setTimeout() closes over name. Even though greet() finishes executing before the timer callback runs, the callback can still access the variable. This is a practical example of closures working with asynchronous JavaScript. Remember that setTimeout() schedules a callback; it doesn't guarantee execution at precisely 1,000 milliseconds.
Question 8: Can Closures Cause Memory Leaks?
Yes, they can contribute to memory leaks when they retain references to objects that an application no longer needs. Consider:
function createHandler() {
const largeData = new Array(1000000).fill("data");
return function handler() {
console.log(largeData.length);
};
}
const handler = createHandler();The returned function needs access to largeData, so the relevant data must remain available while the closure is reachable. If a long-lived event listener or another retained reference keeps the handler alive, the data may remain in memory longer than necessary.
However, closures do not automatically cause memory leaks. JavaScript garbage collection can reclaim unreachable objects and environments when they are no longer needed. To avoid unnecessary memory retention:
- Remove event listeners when they are no longer needed.
- Avoid keeping obsolete callbacks in long-lived objects.
- Release references to data that is no longer required.
- Use browser memory profiling tools when investigating suspected leaks.
6. Common Misconceptions About Closures
Before wrapping up, let’s clear up a few misunderstandings.
Misconception 1: The outer function must remain alive.
Not in the sense of continuing to execute. The outer function can return, while its necessary lexical environment remains available to a reachable closure.
Misconception 2: Closures copy all variables from the outer scope.
A closure retains access to lexical bindings. It doesn’t necessarily create a separate snapshot of every variable.
Misconception 3: Closures are only useful for data privacy.
They are also useful for callbacks, event handlers, function factories, memoization, and maintaining state.
Misconception 4: Every nested function is a closure that retains its environment forever.
A nested function has access to its lexical scope, but environments and objects can be garbage-collected when they are no longer reachable and needed. Closures do not automatically cause permanent memory retention.
7. Final Thoughts
Closures are one of the foundational concepts in JavaScript. Once you understand how lexical scope works, closures become much easier to reason about. They allow functions to retain access to variables from their surrounding environment, even after the outer function has returned. This simple mechanism enables several powerful programming patterns:
- Encapsulation and private state.
- Event handling and asynchronous callbacks.
- Function factories.
- Memoization and caching.
- State management.
For interviews, pay particular attention to the famous var loop problem, independent counter instances, mutable variable bindings, and asynchronous callbacks. And most importantly, don’t just memorize the outputs.
Understand which variable each function can access, where that variable was defined, and when the callback executes.
Once you can explain those three things, you’ll be able to solve most closure-related interview questions with confidence. Happy coding!








