function outer() {
const message = "Hello"; // in outer's scope
function inner() {
console.log(message); // inner closes over message
}
return inner;
}
const fn = outer();
// outer() has returned, but message is still accessible
fn(); // "Hello"
// Every function is a closure
const x = 10;
const add = (n) => n + x; // closes over module-level x
add(5); // 15
Why it matters: Closures underpin data encapsulation, the module pattern, debounce/throttle, memoization, and React hooks. Every senior-level JavaScript interview asks about them directly or indirectly.
Real applications: React hooks rely entirely on closures — useEffect callbacks capture state and props from the render scope. Event handlers close over component state to know which item was clicked.
Common mistakes: Thinking closures only exist inside nested functions — any function that references variables from an outer scope forms a closure, including top-level module code and arrow functions.
function createMultiplier(factor) {
// factor is captured in the closure
return (n) => n * factor;
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
// Logger factory with configurable prefix
function createLogger(prefix) {
return (msg) => console.log(`[${prefix}] ${msg}`);
}
const info = createLogger("INFO");
const warn = createLogger("WARN");
info("App started"); // "[INFO] App started"
warn("Low memory"); // "[WARN] Low memory"
Why it matters: Function factories are a fundamental functional programming pattern used to create specialized versions of general functions — common in middleware systems, validation libraries, and React utility hooks.
Real applications: React's useCallback hooks, Express middleware factories like rateLimit({ windowMs: 15 * 60 * 1000 }), and Lodash's _.partial() all rely on this factory closure pattern.
Common mistakes: Accidentally sharing state between factory instances by placing mutable variables outside the factory function rather than inside it, causing all instances to reference the same variable.
var in a loop with asynchronous callbacks is the classic closure bug — all closures share the same variable reference because var is function-scoped, not block-scoped. By the time the callbacks execute, the loop has completed and the variable holds its final value. The fix is to use let (block-scoped) or an IIFE to capture each iteration's value independently.
// Bug: var is function-scoped, all callbacks share i
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Output: 3, 3, 3 (not 0, 1, 2)
// Fix 1: let creates a new scope per iteration
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Output: 0, 1, 2
// Fix 2: IIFE captures i by value (legacy approach)
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(() => console.log(j), 100);
})(i);
}
// Output: 0, 1, 2
Why it matters: This is one of the most famous JavaScript interview questions. It tests understanding of closures, variable scoping, and var vs let — three fundamental concepts in one compact example.
Real applications: The same bug appears with var in any loop that registers event listeners, creates callbacks, or spawns async operations — a common mistake when adding click handlers to multiple DOM elements in a loop.
Common mistakes: Assuming let in a loop is purely cosmetic — it actually creates a new binding per iteration, which is the mechanism that fixes the closure capture issue. This is a spec-level behavior, not just a different syntax.
function createBankAccount(initialBalance) {
let balance = initialBalance; // private
return {
deposit(amount) {
if (amount > 0) balance += amount;
},
withdraw(amount) {
if (amount > 0 && amount <= balance) balance -= amount;
else throw new Error("Insufficient funds");
},
getBalance() { return balance; }
};
}
const account = createBankAccount(100);
account.deposit(50);
account.withdraw(30);
console.log(account.getBalance()); // 120
// account.balance // undefined — private!
Why it matters: Private state via closures guarantees data integrity — all mutations go through validated methods. Interviewers use this to test whether candidates understand both closures and encapsulation principles simultaneously.
Real applications: Redux store internals use closures to keep state private. RxJS Observable internals hide subscription state. Any scenario needing validation before state mutation benefits from this pattern.
Common mistakes: ES2022 introduced private class fields (#field) as a native alternative — prefer them in class-based code. Closure-based privacy is ideal for functional patterns but should not be retrofitted into existing class hierarchies.
const ShoppingCart = (() => {
const items = []; // private
let total = 0; // private
return {
addItem(name, price) {
items.push({ name, price });
total += price;
},
removeItem(name) {
const idx = items.findIndex(i => i.name === name);
if (idx !== -1) { total -= items[idx].price; items.splice(idx, 1); }
},
getTotal() { return total; },
getItems() { return [...items]; } // return copy, not reference
};
})();
ShoppingCart.addItem("Book", 20);
ShoppingCart.addItem("Pen", 5);
console.log(ShoppingCart.getTotal()); // 25
// ShoppingCart.items // undefined — private
Why it matters: The Module pattern remains prevalent in legacy codebases and any environment without a bundler. Understanding it demonstrates mastery of closures and IIFE patterns that appear throughout the JavaScript ecosystem.
Real applications: jQuery uses a module-pattern IIFE to expose $ without polluting global scope. Browser-targeted libraries bundled with Rollup/UMD form often produce this pattern for the browser global export.
Common mistakes: Returning a direct reference to a private array or object instead of a copy — external code can then mutate the private state. Always return [...arr] or {...obj} copies from getters.
function createHandler() {
const largeData = new Array(1_000_000).fill("x"); // 8MB+
// largeData stays in memory as long as handler lives
return function process() {
return largeData.length;
};
}
let handler = createHandler();
handler(); // 1000000
handler = null; // release reference → largeData can be GC'd
// DOM leak pattern
function setupWidget() {
const bigConfig = loadHeavyConfig(); // large object
const el = document.getElementById("widget");
el.addEventListener("click", () => {
render(bigConfig); // bigConfig kept alive by event listener
});
// Fix: remove listener when widget is destroyed
}
Why it matters: Memory leaks from closures retaining DOM elements are a leading cause of performance degradation in long-running SPAs. DevTools Memory panel can profile retained closures to identify leaks.
Real applications: Event listeners in Angular components that aren't removed in ngOnDestroy, React callbacks capturing Redux store references, and timer callbacks holding component state after unmounting.
Common mistakes: Adding an event listener inside a function and never removing it — the event listener's closure retains all variables from the enclosing scope even after the component or page section is logically "done".
count variable and returns an object of methods that can read and mutate it. Each call to the factory creates a completely independent counter instance with its own private state, demonstrating how closures enable stateful objects without classes.
function createCounter(initial = 0) {
let count = initial; // private, persists between calls
return {
increment() { return ++count; },
decrement() { return --count; },
reset() { count = initial; return count; },
getValue() { return count; }
};
}
const counterA = createCounter(0);
const counterB = createCounter(100); // independent state
counterA.increment(); // 1
counterA.increment(); // 2
counterB.decrement(); // 99
console.log(counterA.getValue()); // 2
console.log(counterB.getValue()); // 99
// counterA and counterB have entirely separate counts
Why it matters: Counters demonstrate the core closure property: persistent private state. Nearly every interview that covers closures includes a counter question as a warm-up or the primary problem.
Real applications: ID generators, request sequence numbers, unique component keys in React list rendering, and analytics event counters all use this pattern. Redux createStore uses a similar closure for private state.
Common mistakes: Placing the counter variable outside the factory — making it shared across all counter instances. Each call to the factory function must create its own let count binding to get truly independent instances.
timeoutId between calls, allowing clearTimeout to cancel the previous timer before scheduling a new one. Only the final call in a burst of rapid invocations actually executes.
function debounce(fn, delay) {
let timeoutId; // private to the closure
return function(...args) {
clearTimeout(timeoutId); // cancel previous
timeoutId = setTimeout(() => {
fn.apply(this, args); // call after delay
}, delay);
};
}
const handleSearch = debounce(async (query) => {
const results = await fetch(`/api/search?q=${query}`);
renderResults(await results.json());
}, 300);
// User types fast: only last call fires after 300ms pause
input.addEventListener("input", (e) => handleSearch(e.target.value));
Why it matters: Without debouncing, every keystroke in a search input would fire an API call. Debounce reduces server load from potentially hundreds of requests per second down to one per pause, critical for search-as-you-type UIs.
Real applications: Google Search autocomplete, Slack message draft autosave, window resize handlers, form validation on blur, and address autocomplete in checkout flows all use debouncing.
Common mistakes: Creating a new debounced function inside a React render function — each render creates a fresh closure with a new timeoutId, breaking the debounce entirely. Wrap with useCallback or useMemo to preserve the stable reference.
function setupToggle(buttonId, label) {
const btn = document.getElementById(buttonId);
let isActive = false; // private per button
btn.addEventListener("click", function() {
isActive = !isActive;
btn.textContent = isActive ? `${label} ON` : `${label} OFF`;
btn.classList.toggle("active", isActive);
});
}
setupToggle("darkMode", "Dark Mode");
setupToggle("notifications", "Notifications");
// Each button tracks its own isActive state independently
// Dynamic list handler pattern
function attachHandlers(items) {
items.forEach((item, idx) => {
item.el.addEventListener("click", () => {
// idx is captured per iteration (let in forEach)
console.log(`Clicked item ${idx}: ${item.name}`);
});
});
}
Why it matters: Event-driven UIs require each handler to remember its own context (which tab, which list item, which user). Closures make this natural without resorting to data-* attributes or global maps.
Real applications: React synthetic event handlers close over props and state, Vue method handlers close over component data, and vanilla JS list item click handlers close over the item's index or ID.
Common mistakes: Using var instead of let in loops when attaching event listeners — all handlers end up sharing the same variable reference, capturing the final loop value instead of the per-iteration value.
lastCall (timestamp of last execution), preserved between invocations to calculate whether enough time has passed.
function throttle(fn, limit) {
let lastCall = 0;
return function(...args) {
const now = Date.now();
if (now - lastCall >= limit) {
lastCall = now;
return fn.apply(this, args);
}
};
}
const updateUI = throttle((scrollY) => {
document.querySelector(".progress").style.width =
(scrollY / document.body.scrollHeight * 100) + "%";
}, 100); // at most 10 updates/second
window.addEventListener("scroll", () => updateUI(window.scrollY));
// vs debounce comparison
// debounce: fires after user STOPS scrolling (300ms quiet)
// throttle: fires every 100ms WHILE user is scrolling
Why it matters: Throttle vs debounce is a classic interview question. Throttle is for regular sampling during continuous events (scroll, resize, mousemove) where you need periodic updates, not just the final value.
Real applications: Twitter's infinite scroll, Google Maps pan/zoom updates, game loop input handlers, and real-time analytics dashboards that sample user interaction data at controlled rates.
Common mistakes: Confusing throttle with debounce — if you use debounce on a scroll handler, the UI only updates after scrolling stops (jarring UX). Throttle gives smooth periodic updates; debounce fires once after activity ends.
Function.prototype.bind() provides built-in partial application.
function partial(fn, ...preset) {
return (...later) => fn(...preset, ...later);
}
function request(method, url, data) {
return fetch(url, { method, body: JSON.stringify(data) });
}
// Specialize with preset arguments
const get = partial(request, "GET");
const post = partial(request, "POST");
get("/api/users"); // GET /api/users
post("/api/users", { name: "Alice" }); // POST /api/users
// Native bind() for partial application
const logError = console.error.bind(console, "[ERROR]");
logError("File not found"); // "[ERROR] File not found"
// Currying vs partial: curry always returns unary functions
const add = a => b => a + b; // curried
const add5 = add(5); // add5(3) === 8
Why it matters: Partial application and currying are functional programming cornerstones tested in senior interviews. They demonstrate understanding of higher-order functions, closures, and how to create reusable, composable utilities.
Real applications: React's React.memo(Component, compareFunc) partially applies comparison, Redux's connect(mapState) is partial application, and HTTP client libraries use it to pre-configure base URLs and headers.
Common mistakes: Confusing partial application with currying — currying always transforms a function into a chain of unary functions (f(a)(b)(c)), while partial application pre-fills any number of arguments at once.
// Scope: x is visible only inside demo
function demo() {
const x = 10;
console.log(x); // OK
}
// console.log(x); // ReferenceError — outside x's scope
// Closure: count persists BEYOND makeCounter's lifetime
function makeCounter() {
let count = 0; // limited scope: makeCounter
return () => ++count; // closure keeps count alive
}
const counter = makeCounter(); // makeCounter has returned!
console.log(counter()); // 1 (count still accessible)
console.log(counter()); // 2 (count incremented)
// No closure — count resets every call
function noClose() {
let count = 0;
return ++count; // count dies when noClose returns
}
console.log(noClose()); // 1
console.log(noClose()); // 1 (always resets)
Why it matters: Interviewers explicitly ask "what is the difference between scope and closure?" to filter candidates who understand closures at a conceptual level vs those who just know code patterns.
Real applications: Understanding this distinction is essential when debugging stale closure bugs in React — hooks capture scope at render time (closure), but the scope rules determine which variables are in scope to be captured.
Common mistakes: Treating closure as a special opt-in feature you have to explicitly "create" — in reality, every function forms a closure automatically over its surrounding scope. There is no special syntax; it is just how JavaScript works.
setTimeout/setInterval) form a closure over their enclosing scope — the callback captures a reference to variables, not their value at registration time. This means the variable value read when the callback fires may differ from when it was registered, especially in loops or stateful async sequences.
// Closure captures reference — value at fire time matters
let msg = "Hello";
setTimeout(() => console.log(msg), 1000);
msg = "Changed"; // Changes BEFORE timeout fires
// After 1s: "Changed" (not "Hello")
// Loop: let creates per-iteration binding (correct)
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), i * 1000); // 0, 1, 2
}
// Cancellable polling pattern using closures
function createPoller(fn, interval) {
let timerId; // private to the closure
return {
start() { timerId = setInterval(fn, interval); },
stop() { clearInterval(timerId); }
};
}
const poller = createPoller(() => syncData(), 5000);
poller.start();
// Later: poller.stop() to cancel
Why it matters: Timer callbacks are a classic source of bugs when combined with mutable variables. This is especially important in React where useEffect closures can capture stale state if the effect is not properly configured.
Real applications: Polling patterns in websocket reconnection logic, session timeout warnings, animation loops, and countdown timers all use closures to maintain state between timer ticks.
Common mistakes: Forgetting to call clearInterval when a component unmounts or a timer is no longer needed — the closed-over variables (including DOM references) stay in memory as long as the interval is active.
// Trap 1: var loop — all closures share one binding
const fns = [];
for (var i = 0; i < 3; i++) fns.push(() => i);
fns.map(f => f()); // [3, 3, 3] — trap!
// Fix: use let for per-iteration binding
for (let i = 0; i < 3; i++) fns.push(() => i);
// Trap 2: stale closure in React useEffect
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // count is STALE — always 0!
}, 1000);
return () => clearInterval(id);
}, []); // empty deps snapshot count at 0 forever
}
// Fix: use functional update to avoid stale reads
setCount(prev => prev + 1); // reads fresh value every tick
Why it matters: Stale closure bugs in React are among the hardest to debug — the UI appears working but produces wrong values. The react-hooks/exhaustive-deps ESLint rule exists specifically to catch these.
Real applications: Polling hooks that read state inside setInterval, event listeners registered in useEffect that read stale props, and memoized callbacks that don't update when dependencies change.
Common mistakes: Believing that adding a dependency to useEffect's array always fixes stale closures — sometimes you need the functional update pattern (setState(prev => ...)) or useRef to hold a mutable reference instead.
called flag and cached result are stored in the closure and persist between invocations. Subsequent calls skip execution and return the cached result immediately.
function once(fn) {
let called = false;
let result;
return function(...args) {
if (!called) {
result = fn.apply(this, args);
called = true;
}
return result;
};
}
const initDB = once(async () => {
console.log("Connecting to DB...");
return await connectDatabase();
});
await initDB(); // "Connecting to DB..." + connection
await initDB(); // cached result returned, no re-connect
await initDB(); // same — DB only connects once
// Native DOM equivalent
btn.addEventListener("click", handleFirst, { once: true });
// Lodash: _.once(fn)
Why it matters: The once pattern is asked in interviews alongside debounce and throttle as a set of "implement this utility" questions. It tests closure knowledge, flag state management, and return value caching.
Real applications: Database connection initialization, SDK setup (Stripe loadStripe() should only run once), Google Analytics tag injection, and feature flag initialization that must run exactly once per session.
Common mistakes: Not caching the return value — the once wrapper should return the original result on all subsequent calls, not undefined. Always store and return result even after setting the flag.