JavaScript — Closures
A closure is the combination of a function bundled together with references to its surrounding state (the lexical environment). In simpler terms, a closure gives you access to an outer function's scope from an inner function. Closures are created every time a function is created — at function creation time, JavaScript captures the current scope chain.
Closures are one of the most powerful and misunderstood features of JavaScript. They enable data privacy, function factories, the module pattern, currying, and many other advanced programming techniques. Understanding closures is essential for mastering JavaScript — they are used everywhere in modern code, from React hooks to middleware chains to event handlers.
Despite their power, closures have memory implications. A closure keeps references to its outer variables, preventing them from being garbage collected as long as the closure exists. This is usually desirable, but can lead to memory leaks if not managed properly.
| 1 | // A closure captures the scope where the function was defined |
| 2 | function outer() { |
| 3 | const message = "Hello from closure!"; |
| 4 | |
| 5 | function inner() { |
| 6 | console.log(message); // inner "closes over" message |
| 7 | } |
| 8 | |
| 9 | return inner; |
| 10 | } |
| 11 | |
| 12 | const myClosure = outer(); |
| 13 | myClosure(); // "Hello from closure!" |
| 14 | // outer() has finished executing, but message is still accessible |
| 15 | |
| 16 | // Every function in JavaScript is a closure |
| 17 | // Even top-level functions close over the global scope |
Every function in JavaScript has a hidden internal property [[Environment]] that references the lexical environment where the function was created. When a function is called, a new execution context is created, and its outer environment reference is set to this stored lexical environment. This is how closures work under the hood — the function carries its birthplace scope with it wherever it goes.
| 1 | // The scope chain in action |
| 2 | const global = "global"; // Level 1: Global scope |
| 3 | |
| 4 | function outer(a) { |
| 5 | const outerVar = "outer"; // Level 2: Outer function scope |
| 6 | |
| 7 | function middle(b) { |
| 8 | const middleVar = "middle"; // Level 3: Middle function scope |
| 9 | |
| 10 | function inner(c) { |
| 11 | const innerVar = "inner"; // Level 4: Inner function scope |
| 12 | |
| 13 | // inner can access ALL levels |
| 14 | console.log(global); // "global" — Level 1 |
| 15 | console.log(a); // "A" — Level 2 (parameter) |
| 16 | console.log(outerVar); // "outer" — Level 2 |
| 17 | console.log(b); // "B" — Level 3 (parameter) |
| 18 | console.log(middleVar); // "middle" — Level 3 |
| 19 | console.log(c); // "C" — Level 4 (parameter) |
| 20 | console.log(innerVar); // "inner" — Level 4 |
| 21 | } |
| 22 | |
| 23 | return inner; |
| 24 | } |
| 25 | |
| 26 | return middle; |
| 27 | } |
| 28 | |
| 29 | const middleFn = outer("A"); |
| 30 | const innerFn = middleFn("B"); |
| 31 | innerFn("C"); |
| 32 | |
| 33 | // Each call creates a new closure with its own captured state |
| 34 | const middleFn2 = outer("X"); |
| 35 | const innerFn2 = middleFn2("Y"); |
| 36 | innerFn2("Z"); // Different captured values |
info
Closures are not just a theoretical concept — they solve real engineering problems. Data privacy, function factories, and the module pattern are among the most practical applications. Understanding these patterns will help you recognize when closures are the right tool.
Data Privacy (Encapsulation)
| 1 | // DATA PRIVACY — simulate private variables |
| 2 | function createCounter() { |
| 3 | let count = 0; // Private — cannot be accessed from outside |
| 4 | |
| 5 | return { |
| 6 | increment() { count++; }, |
| 7 | decrement() { count--; }, |
| 8 | getCount() { return count; }, |
| 9 | reset() { count = 0; } |
| 10 | }; |
| 11 | } |
| 12 | |
| 13 | const counter = createCounter(); |
| 14 | counter.increment(); |
| 15 | counter.increment(); |
| 16 | console.log(counter.getCount()); // 2 |
| 17 | console.log(counter.count); // undefined — private! |
| 18 | // No way to access or modify count except through the returned methods |
| 19 | |
| 20 | // Bank account — stronger privacy example |
| 21 | function createBankAccount(initialBalance = 0) { |
| 22 | let balance = initialBalance; |
| 23 | const transactions = []; |
| 24 | |
| 25 | return { |
| 26 | deposit(amount) { |
| 27 | if (amount <= 0) throw new Error("Invalid amount"); |
| 28 | balance += amount; |
| 29 | transactions.push({ type: "deposit", amount, date: Date.now() }); |
| 30 | return balance; |
| 31 | }, |
| 32 | withdraw(amount) { |
| 33 | if (amount > balance) throw new Error("Insufficient funds"); |
| 34 | balance -= amount; |
| 35 | transactions.push({ type: "withdrawal", amount, date: Date.now() }); |
| 36 | return balance; |
| 37 | }, |
| 38 | getBalance() { return balance; }, |
| 39 | getTransactionHistory() { return [...transactions]; } |
| 40 | }; |
| 41 | } |
| 42 | |
| 43 | const account = createBankAccount(1000); |
| 44 | account.deposit(500); |
| 45 | account.withdraw(200); |
| 46 | console.log(account.getBalance()); // 1300 |
| 47 | console.log(account.balance); // undefined — truly private |
Function Factories
| 1 | // FUNCTION FACTORIES — create customized functions |
| 2 | function createMultiplier(factor) { |
| 3 | return (number) => number * factor; |
| 4 | } |
| 5 | |
| 6 | const double = createMultiplier(2); |
| 7 | const triple = createMultiplier(3); |
| 8 | const tenTimes = createMultiplier(10); |
| 9 | |
| 10 | console.log(double(5)); // 10 |
| 11 | console.log(triple(5)); // 15 |
| 12 | console.log(tenTimes(5)); // 50 |
| 13 | |
| 14 | // URL builder factory |
| 15 | function createUrlBuilder(baseUrl) { |
| 16 | return (endpoint, params = {}) => { |
| 17 | const query = Object.entries(params) |
| 18 | .map(([k, v]) => `${k}=${encodeURIComponent(v)}`) |
| 19 | .join("&"); |
| 20 | return `${baseUrl}/${endpoint}${query ? "?" + query : ""}`; |
| 21 | }; |
| 22 | } |
| 23 | |
| 24 | const api = createUrlBuilder("https://api.example.com/v1"); |
| 25 | console.log(api("users", { page: 1, limit: 10 })); |
| 26 | // https://api.example.com/v1/users?page=1&limit=10 |
| 27 | |
| 28 | // Logging factory |
| 29 | function createLogger(prefix) { |
| 30 | return (message) => { |
| 31 | console.log(`[${prefix.toUpperCase()}] ${message}`); |
| 32 | }; |
| 33 | } |
| 34 | |
| 35 | const info = createLogger("info"); |
| 36 | const warn = createLogger("warn"); |
| 37 | const error = createLogger("error"); |
| 38 | |
| 39 | info("Server started"); // [INFO] Server started |
| 40 | warn("Low memory"); // [WARN] Low memory |
| 41 | error("Connection lost"); // [ERROR] Connection lost |
Event Handlers & Callbacks
| 1 | // Closures in event handlers — capturing state at setup time |
| 2 | function setupButtons() { |
| 3 | const buttons = document.querySelectorAll("button"); |
| 4 | |
| 5 | for (let i = 0; i < buttons.length; i++) { |
| 6 | buttons[i].addEventListener("click", () => { |
| 7 | console.log(`Button ${i} clicked`); // Closes over i |
| 8 | }); |
| 9 | } |
| 10 | } |
| 11 | |
| 12 | // Each button gets its own closure with the correct i value |
| 13 | // Without closures: all handlers would log the same final i |
| 14 | |
| 15 | // Practical: debounce with closure |
| 16 | function debounce(fn, delay = 300) { |
| 17 | let timer = null; // Captured by the returned function |
| 18 | |
| 19 | return function(...args) { |
| 20 | clearTimeout(timer); |
| 21 | timer = setTimeout(() => { |
| 22 | fn(...args); |
| 23 | }, delay); |
| 24 | }; |
| 25 | } |
| 26 | |
| 27 | // Practical: throttle with closure |
| 28 | function throttle(fn, delay = 100) { |
| 29 | let lastCall = 0; // Captured by the returned function |
| 30 | |
| 31 | return function(...args) { |
| 32 | const now = Date.now(); |
| 33 | if (now - lastCall >= delay) { |
| 34 | lastCall = now; |
| 35 | fn(...args); |
| 36 | } |
| 37 | }; |
| 38 | } |
| 39 | |
| 40 | // Practical: once — ensure function runs only once |
| 41 | function once(fn) { |
| 42 | let called = false; |
| 43 | let result; |
| 44 | |
| 45 | return function(...args) { |
| 46 | if (!called) { |
| 47 | called = true; |
| 48 | result = fn(...args); |
| 49 | } |
| 50 | return result; |
| 51 | }; |
| 52 | } |
| 53 | |
| 54 | const initialize = once(() => { |
| 55 | console.log("Initialized!"); |
| 56 | return { ready: true }; |
| 57 | }); |
| 58 | |
| 59 | console.log(initialize()); // "Initialized!" { ready: true } |
| 60 | console.log(initialize()); // { ready: true } — skipped execution |
pro tip
The classic closure-in-a-loop problem is a rite of passage for JavaScript developers. When using var in a loop, all closures created in the loop share the same variable reference — they see the final value after the loop completes. Using let (block scoping) creates a new binding for each iteration, giving each closure its own value.
| 1 | // THE CLASSIC PROBLEM — var in a loop |
| 2 | const buttons = []; |
| 3 | for (var i = 0; i < 5; i++) { |
| 4 | buttons.push(function() { |
| 5 | console.log(`Button ${i} clicked`); |
| 6 | }); |
| 7 | } |
| 8 | |
| 9 | buttons[0](); // "Button 5 clicked" — expected 0! |
| 10 | buttons[2](); // "Button 5 clicked" — expected 2! |
| 11 | // All closures reference the same `i` variable: the final value 5 |
| 12 | |
| 13 | // FIX 1: Use let (ES6) — block-scoped per iteration |
| 14 | const buttonsLet = []; |
| 15 | for (let i = 0; i < 5; i++) { |
| 16 | // let creates a new binding for EACH iteration |
| 17 | buttonsLet.push(function() { |
| 18 | console.log(`Button ${i} clicked`); |
| 19 | }); |
| 20 | } |
| 21 | |
| 22 | buttonsLet[0](); // "Button 0 clicked" ✓ |
| 23 | buttonsLet[2](); // "Button 2 clicked" ✓ |
| 24 | |
| 25 | // FIX 2: IIFE (Immediately Invoked Function Expression) — pre-ES6 |
| 26 | const buttonsIIFE = []; |
| 27 | for (var i = 0; i < 5; i++) { |
| 28 | (function(index) { // IIFE captures the current value |
| 29 | buttonsIIFE.push(function() { |
| 30 | console.log(`Button ${index} clicked`); |
| 31 | }); |
| 32 | })(i); |
| 33 | } |
| 34 | |
| 35 | buttonsIIFE[0](); // "Button 0 clicked" ✓ |
| 36 | |
| 37 | // FIX 3: Closure factory |
| 38 | function createButtonHandler(index) { |
| 39 | return function() { |
| 40 | console.log(`Button ${index} clicked`); |
| 41 | }; |
| 42 | } |
| 43 | |
| 44 | const buttonsFactory = []; |
| 45 | for (var i = 0; i < 5; i++) { |
| 46 | buttonsFactory.push(createButtonHandler(i)); |
| 47 | } |
| 48 | |
| 49 | buttonsFactory[0](); // "Button 0 clicked" ✓ |
best practice
Closures keep references to their outer scope variables alive as long as the closure exists. This means the garbage collector cannot free those variables — they remain in memory. This is necessary for correctness, but can lead to memory leaks if closures are unintentionally retained. Understanding when and how closures hold memory is critical for writing performant applications.
| 1 | // Closures keep variables alive |
| 2 | function createBigData() { |
| 3 | const bigArray = new Array(1000000).fill("data"); |
| 4 | const bigString = "x".repeat(1000000); |
| 5 | |
| 6 | return function() { |
| 7 | // Even if we only use a small part, the WHOLE scope is retained |
| 8 | console.log(bigArray.length); |
| 9 | }; |
| 10 | } |
| 11 | |
| 12 | const closure = createBigData(); |
| 13 | // bigArray and bigString cannot be garbage collected |
| 14 | // as long as `closure` exists |
| 15 | |
| 16 | // Minimize closure scope — only capture what you need |
| 17 | function createEfficient() { |
| 18 | const bigArray = new Array(1000000).fill("data"); |
| 19 | const smallValue = 42; // We only need this |
| 20 | |
| 21 | // Only smallValue is captured (if bigArray is not referenced) |
| 22 | return function() { |
| 23 | return smallValue; // Only closes over smallValue |
| 24 | }; |
| 25 | } |
| 26 | |
| 27 | // Common memory leak: accidental closure in event listeners |
| 28 | class Component { |
| 29 | constructor() { |
| 30 | this.data = loadLargeData(); |
| 31 | |
| 32 | // This creates a closure that keeps `this` (the component) alive |
| 33 | document.addEventListener("click", () => { |
| 34 | this.handleClick(); // Closure captures `this` |
| 35 | }); |
| 36 | // If the component is removed, the listener holds a reference |
| 37 | // preventing garbage collection → MEMORY LEAK |
| 38 | } |
| 39 | |
| 40 | destroy() { |
| 41 | // Must remove the listener to free the closure |
| 42 | document.removeEventListener("click", this.handler); |
| 43 | } |
| 44 | } |
| 45 | |
| 46 | // Memory leak: DOM element references in closures |
| 47 | function processElements() { |
| 48 | const elements = document.querySelectorAll(".item"); |
| 49 | |
| 50 | return function() { |
| 51 | // Each element in `elements` is retained by this closure |
| 52 | console.log(elements.length); |
| 53 | }; |
| 54 | } |
| 55 | |
| 56 | // Better: release references when no longer needed |
| 57 | function createSafeProcessor() { |
| 58 | let elements = document.querySelectorAll(".item"); |
| 59 | |
| 60 | function processor() { |
| 61 | if (!elements) return; |
| 62 | console.log(elements.length); |
| 63 | } |
| 64 | |
| 65 | processor.release = function() { |
| 66 | elements = null; // Allow garbage collection |
| 67 | }; |
| 68 | |
| 69 | return processor; |
| 70 | } |
warning
Before ES modules and classes, the module pattern was the standard way to create encapsulated, self-contained units of code. It uses an IIFE (Immediately Invoked Function Expression) that returns an object with public methods. The IIFE creates a closure for private state, exposing only what the returned object provides — a clean public API with private implementation details.
| 1 | // CLASSIC MODULE PATTERN — IIFE returning a public API |
| 2 | const UserModule = (function() { |
| 3 | // Private state — cannot be accessed from outside |
| 4 | const users = []; |
| 5 | let nextId = 1; |
| 6 | |
| 7 | // Private helper function |
| 8 | function validateUser(user) { |
| 9 | if (!user.name || typeof user.name !== "string") { |
| 10 | throw new Error("Invalid user name"); |
| 11 | } |
| 12 | if (user.age !== undefined && (user.age < 0 || user.age > 150)) { |
| 13 | throw new Error("Invalid age"); |
| 14 | } |
| 15 | } |
| 16 | |
| 17 | // Private utility |
| 18 | function formatUser(user) { |
| 19 | return { ...user, createdAt: new Date().toISOString() }; |
| 20 | } |
| 21 | |
| 22 | // Public API — returned object |
| 23 | return { |
| 24 | addUser(name, age) { |
| 25 | const user = formatUser({ id: nextId++, name, age }); |
| 26 | validateUser(user); |
| 27 | users.push(user); |
| 28 | return user; |
| 29 | }, |
| 30 | |
| 31 | getUser(id) { |
| 32 | return users.find(u => u.id === id) || null; |
| 33 | }, |
| 34 | |
| 35 | getAllUsers() { |
| 36 | return [...users]; // Return copy to prevent external mutation |
| 37 | }, |
| 38 | |
| 39 | removeUser(id) { |
| 40 | const index = users.findIndex(u => u.id === id); |
| 41 | if (index !== -1) { |
| 42 | users.splice(index, 1); |
| 43 | return true; |
| 44 | } |
| 45 | return false; |
| 46 | }, |
| 47 | |
| 48 | get count() { |
| 49 | return users.length; |
| 50 | } |
| 51 | }; |
| 52 | })(); |
| 53 | |
| 54 | console.log(UserModule.addUser("Alice", 30)); |
| 55 | console.log(UserModule.addUser("Bob", 25)); |
| 56 | console.log(UserModule.count); // 2 |
| 57 | console.log(UserModule.getAllUsers()); |
| 58 | // console.log(UserModule.users); // undefined — private! |
| 59 | // console.log(UserModule.nextId); // undefined — private! |
| 60 | |
| 61 | // REVEALING MODULE PATTERN — define all functions privately, reveal by name |
| 62 | const Calculator = (function() { |
| 63 | // All functions private initially |
| 64 | const add = (a, b) => a + b; |
| 65 | const subtract = (a, b) => a - b; |
| 66 | const multiply = (a, b) => a * b; |
| 67 | const divide = (a, b) => { |
| 68 | if (b === 0) throw new Error("Division by zero"); |
| 69 | return a / b; |
| 70 | }; |
| 71 | const privateLog = (op, a, b, result) => { |
| 72 | console.log(`${op}: ${a} ${b} = ${result}`); |
| 73 | }; |
| 74 | |
| 75 | // Reveal only what should be public |
| 76 | return { |
| 77 | add: (a, b) => { |
| 78 | const result = add(a, b); |
| 79 | privateLog("Add", a, b, result); |
| 80 | return result; |
| 81 | }, |
| 82 | subtract: (a, b) => { |
| 83 | const result = subtract(a, b); |
| 84 | privateLog("Subtract", a, b, result); |
| 85 | return result; |
| 86 | }, |
| 87 | multiply, |
| 88 | divide |
| 89 | }; |
| 90 | })(); |
| 91 | |
| 92 | console.log(Calculator.add(5, 3)); // 8 |
| 93 | // Calculator.privateLog is not accessible |
info
Currying transforms a function that takes multiple arguments into a sequence of functions, each taking a single argument. Each function in the chain returns another function until all arguments have been supplied. Closures are the mechanism that makes currying work — each intermediate function closes over the accumulated arguments.
| 1 | // MANUAL CURRYING — each function returns the next |
| 2 | function add(a) { |
| 3 | return function(b) { |
| 4 | return function(c) { |
| 5 | return a + b + c; |
| 6 | }; |
| 7 | }; |
| 8 | } |
| 9 | |
| 10 | console.log(add(1)(2)(3)); // 6 |
| 11 | |
| 12 | // Arrow function syntax — more concise |
| 13 | const addArrow = a => b => c => a + b + c; |
| 14 | console.log(addArrow(1)(2)(3)); // 6 |
| 15 | |
| 16 | // CURRYING UTILITY — convert any function to curried form |
| 17 | function curry(fn) { |
| 18 | return function curried(...args) { |
| 19 | if (args.length >= fn.length) { |
| 20 | return fn(...args); |
| 21 | } |
| 22 | // Return a function that waits for more arguments |
| 23 | return (...moreArgs) => curried(...args, ...moreArgs); |
| 24 | }; |
| 25 | } |
| 26 | |
| 27 | const curriedAdd = curry((a, b, c) => a + b + c); |
| 28 | console.log(curriedAdd(1)(2)(3)); // 6 |
| 29 | console.log(curriedAdd(1, 2)(3)); // 6 |
| 30 | console.log(curriedAdd(1, 2, 3)); // 6 |
| 31 | |
| 32 | // Practical: curried logger |
| 33 | const log = (level) => (message) => `[${level.toUpperCase()}] ${message}`; |
| 34 | |
| 35 | const info = log("info"); |
| 36 | const warn = log("warn"); |
| 37 | const error = log("error"); |
| 38 | |
| 39 | console.log(info("Server started")); // [INFO] Server started |
| 40 | console.log(error("Connection lost")); // [ERROR] Connection lost |
| 41 | |
| 42 | // Practical: curried HTTP request builder |
| 43 | const request = (method) => (url) => (data) => { |
| 44 | return fetch(url, { |
| 45 | method, |
| 46 | headers: { "Content-Type": "application/json" }, |
| 47 | body: data ? JSON.stringify(data) : undefined |
| 48 | }).then(r => r.json()); |
| 49 | }; |
| 50 | |
| 51 | const get = request("GET"); |
| 52 | const post = request("POST"); |
| 53 | |
| 54 | // get("/api/users") returns a function expecting no data |
| 55 | // post("/api/users")({ name: "Alice" }) makes a POST request |
| 56 | |
| 57 | // Partial application — similar but different from currying |
| 58 | function partial(fn, ...presetArgs) { |
| 59 | return function(...laterArgs) { |
| 60 | return fn(...presetArgs, ...laterArgs); |
| 61 | }; |
| 62 | } |
| 63 | |
| 64 | const multiply = (a, b, c) => a * b * c; |
| 65 | const multiplyBy2 = partial(multiply, 2); |
| 66 | console.log(multiplyBy2(3, 4)); // 24 — 2 * 3 * 4 |
info