Ever been baffled when an inner function still accesses variables after its outer function has returned?
That’s closures in JavaScript, and they’re less mysterious than people make them.
Think of a closure as a function carrying a backpack of its parent’s variables so it can use them later.
This post explains closures in plain language, shows simple runnable examples (greeting, private counter, timer), and points out common pitfalls so you can use closures with confidence.
Plain-Language Closure Explanation with Simple Beginner Examples

A closure is an inner function that remembers and can access variables from its parent function’s lexical scope even after the parent function has finished executing. Lexical scope is the rule that determines which variables a function can see, based on where that function was written in your code, not where it’s called. When a nested function references a variable from its parent, JavaScript doesn’t just copy that value. It maintains a live reference back to the parent’s environment, letting the inner function reach those variables later.
Think of a closure like a function carrying a backpack. When you create a function inside another function, the inner function packs up any variables it needs from the outer scope and keeps that “backpack” with it wherever it goes. Even after the outer function finishes running and disappears from the call stack, the inner function still has that backpack and can pull out those variables whenever it needs them.
Closures give you persistent state. Variables remain accessible across multiple calls to the inner function. Inner functions can read and modify variables from their outer function. Closures store references to variables, not static copies, so changes are reflected later. Variable visibility is determined by code structure, not execution order. Closures only work when you nest functions and the inner function uses outer variables.
Here’s the simplest possible closure. An outer function returns an inner function, and the inner function uses a variable from the outer scope:
function outer() {
let message = "Hello";
return function inner() {
console.log(message);
};
}
const greet = outer();
greet(); // prints "Hello"
The inner function remembers message from outer, even after outer has finished executing.
How Lexical Scope and the Scope Chain Make JavaScript Closures Work

Lexical scoping means JavaScript determines which variables a function can see by looking at where the function was defined in your source code. When you nest functions, each inner function can see variables from its own local scope plus every surrounding parent scope all the way up to the global scope. This chain of accessible scopes is called the scope chain, and closures rely on that chain staying intact.
You’ve got local scope, which is variables declared inside the currently executing function. Parent scope covers variables from the function that created the current function. Global scope holds variables declared at the top level of your script. There’s also a hidden internal environment, a special internal reference that JavaScript stores to keep the scope chain alive after a function returns.
JavaScript builds closures in two execution phases. During the Creation Phase, the engine establishes the scope chain and declares all local variables without assigning them values yet. During the Execution Phase, those variables get their actual values and your code runs line by line. When an inner function is created, JavaScript stores an internal reference to the parent environment. That reference keeps all parent variables reachable even after the parent function returns and its execution context is removed from the call stack, so the inner function can still look up those variables later when it runs.
Simple Closure Use Cases Explained for Beginners

One common closure use case is creating private variables. A private variable is data that only your inner function can see and change. Here’s a simple counter that hides its count variable:
function createCounter() {
let count = 0;
return function() {
count++;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
No code outside the returned function can touch count directly. The closure remembers count from createCounter, increments it each time you call the function, and maintains that state across multiple calls.
Another practical pattern is the function factory. A factory function produces customized functions that share common logic but remember different configuration values. Here’s a discount factory that creates specialized discount functions:
function createDiscount(discountRate) {
return function(price) {
return price - (price * discountRate);
};
}
const tenPercentOff = createDiscount(0.10);
const twentyPercentOff = createDiscount(0.20);
console.log(tenPercentOff(100)); // 90
console.log(twentyPercentOff(100)); // 80
Each returned function remembers its own discountRate from the outer scope, letting you build multiple reusable discount functions without repeating logic.
Closures are also essential for asynchronous code like timers and event listeners. When you use setTimeout, the callback function needs to remember values from the current scope even though it runs later. Here’s a reminder function that captures a custom delay:
function createReminder(seconds) {
return function(message) {
setTimeout(() => {
console.log(message);
}, seconds * 1000);
};
}
const remindInThree = createReminder(3);
remindInThree("Time's up!"); // prints after 3 seconds
The timer callback retains seconds from createReminder and message from the returned function, demonstrating how closures preserve state across asynchronous boundaries.
| Use Case | What the Closure Remembers | Simple Example |
|---|---|---|
| Private variables | Local state inaccessible from outside | createCounter retaining count |
| Function factories | Configuration values passed to the factory | createDiscount retaining discountRate |
| Async/timers | Variables from the scope where the timer was created | createReminder retaining seconds |
Understanding Common Closure Pitfalls (For Loops, Timers, and Variable Capture)

The classic closure pitfall happens when you create functions inside a loop using var. Because var is function scoped and not block scoped, every function created in the loop shares the same variable. By the time any of those functions run, the loop has finished and the variable holds its final value. Here’s the broken pattern:
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// prints 3, 3, 3 (not 0, 1, 2)
All three setTimeout callbacks capture the same i, and by the time they run, i is 3.
Functions created in a var loop all reference the same variable, which ends up holding the final loop value. Callbacks scheduled with setTimeout or setInterval remember variables by reference, not by snapshot. Closures resolve variable values when they execute, not when they’re created. var declarations are hoisted to function scope, causing unexpected shared state. Promises, event listeners, and timers run later, so closures must correctly capture the intended variable state. If you forget to declare a variable, closures may capture an unintended global variable instead of a local one.
You can fix the loop problem by using let instead of var. Because let is block scoped, each iteration of the loop creates a fresh binding, and each closure captures its own independent copy:
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// prints 0, 1, 2
Alternatively, you can use an immediately invoked function expression (IIFE) to create a new scope for each iteration and capture the current value. Both techniques ensure that each callback remembers the correct iteration specific value, avoiding late binding surprises.
Practical Step-by-Step Closure Walkthroughs for Beginners

Let’s walk through a UI configuration example called buttonProps. You want to build button objects with a shared border radius but different colors and variants. The outer function buttonProps accepts borderRadius, and the inner function createVariantButtonProps accepts variant and color, then builds a configuration object that includes the borderRadius from the parent scope:
function buttonProps(borderRadius) {
return function createVariantButtonProps(variant, color) {
return {
variant: variant,
color: color,
borderRadius: borderRadius
};
};
}
const roundedButton = buttonProps("8px");
console.log(roundedButton("primary", "blue"));
// { variant: "primary", color: "blue", borderRadius: "8px" }
console.log(roundedButton("secondary", "gray"));
// { variant: "secondary", color: "gray", borderRadius: "8px" }
Notice that borderRadius is defined once in the outer function, but every call to the inner function can access it. The closure lets you lock in a shared configuration value and reuse it across multiple button objects without passing it every time.
Identify parent scope variables. Look for variables declared in the outer function that the inner function references. Track inner function returns. See what the outer function returns and how the inner function uses those parent variables. Test persistent state. Call the returned function multiple times and observe whether changes to parent variables affect later calls. Modify variables. Try updating a parent variable inside the inner function and confirm that the change persists across subsequent calls.
Now let’s look at a real world fetch utility. The fetchUtility function accepts a baseURL and headers object, then returns createFetchInstance, which accepts route, requestMethod, and optional data. The inner function combines baseURL with route to build the full URL, constructs a request configuration using the shared headers, and returns a fetch instance:
function fetchUtility(baseURL, headers) {
return function createFetchInstance(route, requestMethod, data) {
const url = `${baseURL}${route}`;
const config = {
method: requestMethod,
headers: headers
};
if (data) {
config.body = JSON.stringify(data);
}
return fetch(url, config);
};
}
const jsonAPI = fetchUtility("https://jsonplaceholder.typicode.com", {
"Content-Type": "application/json"
});
jsonAPI("/posts", "GET").then(res => res.json()).then(console.log);
jsonAPI("/posts", "POST", { title: "Test", body: "Content", userId: 1 })
.then(res => res.json())
.then(console.log);
The closure remembers baseURL and headers from fetchUtility, so you configure those once and every call to createFetchInstance automatically uses them. You can create multiple fetch instances with different base URLs and header sets, and each closure keeps its own configuration data without mixing them up.
Closure Memory Considerations and Performance Basics

Variables captured by closures stay reachable in memory until all references to the closure are gone. If your closure holds a reference to a large object or an expensive data structure, that object won’t be garbage collected until the closure itself is eligible for cleanup. This can lead to higher memory usage than you expect, especially if you create many closures that each capture large variables.
Overusing closures can slow your application down because each new closure duplicates the internal environment reference and keeps additional variables in memory. If you create thousands of closures in a tight loop, you may notice increased memory consumption and slower performance. The key is balance. Use closures when they improve readability and modularity, but avoid creating them unnecessarily inside performance critical loops.
Captured variables stay reachable. Variables referenced by closures remain in memory until the closure is no longer referenced. If your closure captures a big object, that object persists as long as the closure exists. Drop references to free memory. Setting a closure variable to null or letting it go out of scope allows garbage collection to reclaim memory.
Quick Beginner-Friendly Closure Reminders

Closures are a natural result of JavaScript’s lexical scoping rules. When you write a function inside another function and the inner function uses a variable from the outer scope, you’ve created a closure. The inner function stores a reference to the parent’s environment, not a static copy of the variables, so changes to those variables are reflected whenever the closure runs. Closures power function factories, UI configuration utilities, private state patterns, async timers, and fetch utilities, making them one of the most versatile features in JavaScript.
Lexical scope determines access. Where a function is defined controls which variables it can see, not where it’s called. Closures capture variables by reference, not copies, so updates to those variables affect all closures that reference them. Closures let you maintain state between function calls without using global variables. Watch out for var in loops, late binding in timers, and accidental global captures. You need private variables, reusable function factories, or persistent configuration in async code.
Final Words
You learned a plain-language definition and a tiny visual analogy showing how inner functions keep access to parent variables. The post walked through lexical scope, the scope chain, simple use cases (private counters, factories, timers), common pitfalls, and memory basics.
Try the small walkthroughs and the four quick test steps to see closures hold state in real code. Test changing variables and watch the inner function keep the value.
This clear explanation of closures in javascript with simple examples for beginners should leave you ready to experiment. Keep going — closures get easier with practice.
FAQ
Q: What is closure in JavaScript in simple words? How do you explain closure?
A: A JavaScript closure, explained simply, is an inner function that remembers and can access variables from its parent function’s lexical scope even after the parent has finished running.
Q: What are examples of closures?
A: Examples of closures are a createCounter that keeps a private count, function factories like createDiscount, async timers or callbacks that retain configuration, and event listeners accessing parent variables.
Q: Why use closures in JavaScript?
A: Closures are used in JavaScript to create private state, build function factories, preserve configuration across calls, and let callbacks or timers access values set earlier.

