|$ curl https://forge-ai.dev/api/markdown?path=docs/js/closures
$cat docs/javascript-—-closures.md
updated Recently·30 min read·published

JavaScript — Closures

JavaScriptClosuresAdvancedAdvanced
Introduction

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.

closure-intro.js
JavaScript
1// A closure captures the scope where the function was defined
2function 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
12const myClosure = outer();
13myClosure(); // "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
Lexical Environment & Mechanics

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.

Closure Scope Chain
inner() ┌─ Local Scope (inner's variables) ├─ [[Environment]] → │ outer() │ ┌─ Local Scope (message, count, etc.) │ ├─ [[Environment]] → │ │ Global Scope │ │ ┌─ window, document, Math, etc. │ │ └─ ... │ └─ closure keeps this alive └─ inner can access ALL three levels
lexical-environment.js
JavaScript
1// The scope chain in action
2const global = "global"; // Level 1: Global scope
3
4function 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
29const middleFn = outer("A");
30const innerFn = middleFn("B");
31innerFn("C");
32
33// Each call creates a new closure with its own captured state
34const middleFn2 = outer("X");
35const innerFn2 = middleFn2("Y");
36innerFn2("Z"); // Different captured values

info

The scope chain is determined by where functions are written in the source code (lexical scoping), not where they are called. This is why closures work regardless of how or where the inner function is later invoked. The function always has access to the scope where it was defined.
Practical Use Cases

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)

data-privacy.js
JavaScript
1// DATA PRIVACY — simulate private variables
2function 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
13const counter = createCounter();
14counter.increment();
15counter.increment();
16console.log(counter.getCount()); // 2
17console.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
21function 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
43const account = createBankAccount(1000);
44account.deposit(500);
45account.withdraw(200);
46console.log(account.getBalance()); // 1300
47console.log(account.balance); // undefined — truly private

Function Factories

function-factories.js
JavaScript
1// FUNCTION FACTORIES — create customized functions
2function createMultiplier(factor) {
3 return (number) => number * factor;
4}
5
6const double = createMultiplier(2);
7const triple = createMultiplier(3);
8const tenTimes = createMultiplier(10);
9
10console.log(double(5)); // 10
11console.log(triple(5)); // 15
12console.log(tenTimes(5)); // 50
13
14// URL builder factory
15function 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
24const api = createUrlBuilder("https://api.example.com/v1");
25console.log(api("users", { page: 1, limit: 10 }));
26// https://api.example.com/v1/users?page=1&limit=10
27
28// Logging factory
29function createLogger(prefix) {
30 return (message) => {
31 console.log(`[${prefix.toUpperCase()}] ${message}`);
32 };
33}
34
35const info = createLogger("info");
36const warn = createLogger("warn");
37const error = createLogger("error");
38
39info("Server started"); // [INFO] Server started
40warn("Low memory"); // [WARN] Low memory
41error("Connection lost"); // [ERROR] Connection lost

Event Handlers & Callbacks

event-handlers.js
JavaScript
1// Closures in event handlers — capturing state at setup time
2function 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
16function 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
28function 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
41function 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
54const initialize = once(() => {
55 console.log("Initialized!");
56 return { ready: true };
57});
58
59console.log(initialize()); // "Initialized!" { ready: true }
60console.log(initialize()); // { ready: true } — skipped execution
🔥

pro tip

Closures are the mechanism behind many utility functions in libraries like Lodash (_.debounce, _.throttle, _.once). Each call to these factories creates a closure that holds the internal state (timer, counter, flag) privately. The returned function can access and update this state on every invocation.
Closures in Loops — var vs let

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.

closure-loop.js
JavaScript
1// THE CLASSIC PROBLEM — var in a loop
2const buttons = [];
3for (var i = 0; i < 5; i++) {
4 buttons.push(function() {
5 console.log(`Button ${i} clicked`);
6 });
7}
8
9buttons[0](); // "Button 5 clicked" — expected 0!
10buttons[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
14const buttonsLet = [];
15for (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
22buttonsLet[0](); // "Button 0 clicked" ✓
23buttonsLet[2](); // "Button 2 clicked" ✓
24
25// FIX 2: IIFE (Immediately Invoked Function Expression) — pre-ES6
26const buttonsIIFE = [];
27for (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
35buttonsIIFE[0](); // "Button 0 clicked" ✓
36
37// FIX 3: Closure factory
38function createButtonHandler(index) {
39 return function() {
40 console.log(`Button ${index} clicked`);
41 };
42}
43
44const buttonsFactory = [];
45for (var i = 0; i < 5; i++) {
46 buttonsFactory.push(createButtonHandler(i));
47}
48
49buttonsFactory[0](); // "Button 0 clicked" ✓

best practice

Always use let instead of var in loops. The let keyword creates a new lexical environment for each iteration, so closures created inside the loop capture the correct value. This is one of the most important practical improvements ES6 brought to JavaScript — it eliminated an entire class of bugs.
Memory Implications

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.

memory-closures.js
JavaScript
1// Closures keep variables alive
2function 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
12const 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
17function 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
28class 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
47function 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
57function 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

Closures are not inherently memory leaks — they are a necessary language feature. Memory leaks happen when closures are held longer than needed. Common sources: event listeners that are never removed, large data structures captured unintentionally, and DOM references in closures that outlive the element. Always clean up event listeners and release closure references when components are destroyed.
The Module Pattern

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.

module-pattern.js
JavaScript
1// CLASSIC MODULE PATTERN — IIFE returning a public API
2const 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
54console.log(UserModule.addUser("Alice", 30));
55console.log(UserModule.addUser("Bob", 25));
56console.log(UserModule.count); // 2
57console.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
62const 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
92console.log(Calculator.add(5, 3)); // 8
93// Calculator.privateLog is not accessible

info

The module pattern is largely superseded by ES modules (import/export) and class syntax, but understanding it is crucial because it demonstrates how closures enable encapsulation. Many libraries and frameworks still use this pattern internally. The revealing module pattern, where all functions are private then selectively exposed, is particularly clean.
Currying with Closures

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.

currying.js
JavaScript
1// MANUAL CURRYING — each function returns the next
2function add(a) {
3 return function(b) {
4 return function(c) {
5 return a + b + c;
6 };
7 };
8}
9
10console.log(add(1)(2)(3)); // 6
11
12// Arrow function syntax — more concise
13const addArrow = a => b => c => a + b + c;
14console.log(addArrow(1)(2)(3)); // 6
15
16// CURRYING UTILITY — convert any function to curried form
17function 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
27const curriedAdd = curry((a, b, c) => a + b + c);
28console.log(curriedAdd(1)(2)(3)); // 6
29console.log(curriedAdd(1, 2)(3)); // 6
30console.log(curriedAdd(1, 2, 3)); // 6
31
32// Practical: curried logger
33const log = (level) => (message) => `[${level.toUpperCase()}] ${message}`;
34
35const info = log("info");
36const warn = log("warn");
37const error = log("error");
38
39console.log(info("Server started")); // [INFO] Server started
40console.log(error("Connection lost")); // [ERROR] Connection lost
41
42// Practical: curried HTTP request builder
43const 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
51const get = request("GET");
52const 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
58function partial(fn, ...presetArgs) {
59 return function(...laterArgs) {
60 return fn(...presetArgs, ...laterArgs);
61 };
62}
63
64const multiply = (a, b, c) => a * b * c;
65const multiplyBy2 = partial(multiply, 2);
66console.log(multiplyBy2(3, 4)); // 24 — 2 * 3 * 4

info

Currying and partial application are related but different. Currying transforms a multi-argument function into a chain of unary functions. Partial application pre-fills some arguments of a function, returning a function that accepts the rest. Both leverage closures for their implementation. Currying is especially useful in functional programming pipelines and when working with point-free style.
$Blueprint — Engineering Documentation·Section ID: JS-CLOSURES·Revision: 1.0