docs/concepts/scope-and-closures.mdx
Why can some variables be accessed from anywhere in your code, while others seem to disappear? How do functions "remember" variables from their parent functions, even after those functions have finished running?
function createCounter() {
let count = 0 // This variable is "enclosed"
return function() {
count++
return count
}
}
const counter = createCounter()
console.log(counter()) // 1
console.log(counter()) // 2 — it remembers!
The answers lie in understanding scope and closures. These two fundamental concepts govern how variables work in JavaScript. Scope determines where variables are visible, while closures allow functions to remember their original environment.
<Info> **What you'll learn in this guide:** - The 3 types of scope: global, function, and block - How [`var`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/var), [`let`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let), and [`const`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const) behave differently - What lexical scope means and how the scope chain works - What closures are and why every JavaScript developer must understand them - Practical patterns: data privacy, factories, and memoization - The classic closure gotchas and how to avoid them </Info> <Warning> **Prerequisite:** This guide builds on your understanding of the [call stack](/concepts/call-stack). Knowing how JavaScript tracks function execution will help you understand how scope and closures work under the hood. </Warning>Scope is the current context of execution in which values and expressions are "visible" or can be referenced. It's the set of rules that determines where and how variables can be accessed in your code. If a variable is not in the current scope, it cannot be used. Scopes can be nested, and inner scopes have access to outer scopes, but not vice versa.
Imagine it's after hours and you're wandering through your office building (legally, you work there, promise). You notice something interesting about what you can and can't see:
┌─────────────────────────────────────────────────────────────┐
│ LOBBY (Global Scope) │
│ reception = "Welcome Desk" │
│ │
│ ┌───────────────────────────────────────────────────┐ │
│ │ HALLWAY (Function Scope) │ │
│ │ hallwayPlant = "Fern" │ │
│ │ │ │
│ │ ┌───────────────────────────────────────┐ │ │
│ │ │ PRIVATE OFFICE (Block Scope) │ │ │
│ │ │ secretDocs = "Confidential" │ │ │
│ │ │ │ │ │
│ │ │ Can see: secretDocs ✓ │ │ │
│ │ │ Can see: hallwayPlant ✓ │ │ │
│ │ │ Can see: reception ✓ │ │ │
│ │ └───────────────────────────────────────┘ │ │
│ │ │ │
│ │ Cannot see: secretDocs ✗ │ │
│ └───────────────────────────────────────────────────┘ │
│ │
│ Cannot see: hallwayPlant, secretDocs ✗ │
└─────────────────────────────────────────────────────────────┘
This is exactly how scope works in JavaScript! Code in inner scopes can "look out" and access variables from outer scopes, but outer scopes can never "look in" to inner scopes.
And here's where it gets really interesting: imagine someone who worked in that private office quits and leaves the building. But they took a mental snapshot of everything in there: the passwords on sticky notes, the secret project plans, the snack drawer location. Even though they've left, they still remember everything. That's essentially what a closure is: a function that "remembers" the scope where it was created, even after that scope is gone.
Scope exists for three critical reasons:
<AccordionGroup> <Accordion title="1. Preventing Naming Conflicts"> Without scope, every variable would be global. Imagine the chaos if every `i` in every `for` loop had to have a unique name!```javascript
function countApples() {
let count = 0; // This 'count' is separate...
// ...
}
function countOranges() {
let count = 0; // ...from this 'count'
// ...
}
```
```javascript
function processData() {
let hugeArray = new Array(1000000); // Takes memory
// ... process it
} // hugeArray can now be garbage collected
```
```javascript
function createBankAccount() {
let balance = 0; // Private! Can't be accessed directly
return {
deposit(amount) { balance += amount; },
getBalance() { return balance; }
};
}
```
JavaScript has three main types of scope. Understanding each one is fundamental to writing predictable code.
<Note> ES6 modules also introduce **module scope**, where top-level variables are scoped to the module rather than being global. Learn more in our [IIFE, Modules and Namespaces](/concepts/iife-modules) guide. </Note>Variables declared outside of any function or block are in the global scope. They're accessible from anywhere in your code.
// Global scope
const appName = "MyApp";
let userCount = 0;
function greet() {
console.log(appName); // ✓ Can access global variable
userCount++; // ✓ Can modify global variable
}
if (true) {
console.log(appName); // ✓ Can access global variable
}
In browsers, global variables become properties of the window object. In Node.js, they attach to global. The modern, universal way to access the global object is globalThis.
var oldSchool = "I'm on window"; // window.oldSchool (var only)
let modern = "I'm NOT on window"; // NOT on window
console.log(window.oldSchool); // "I'm on window"
console.log(window.modern); // undefined
console.log(globalThis); // Works everywhere
// Bad: Polluting global scope
var userData = {};
var settings = {};
var helpers = {};
// Good: Use a single namespace
const MyApp = {
userData: {},
settings: {},
helpers: {}
};
Variables declared with var inside a function are function-scoped. They're only accessible within that function.
function calculateTotal() {
var subtotal = 100;
var tax = 10;
var total = subtotal + tax;
console.log(total); // ✓ 110
}
calculateTotal();
// console.log(subtotal); // ✗ ReferenceError: subtotal is not defined
Variables declared with var are "hoisted" to the top of their function. This means JavaScript knows about them before the code runs, but they're initialized as undefined until the actual declaration line.
function example() {
console.log(message); // undefined (not an error!)
var message = "Hello";
console.log(message); // "Hello"
}
// JavaScript interprets this as:
function exampleHoisted() {
var message; // Declaration hoisted to top
console.log(message); // undefined
message = "Hello"; // Assignment stays in place
console.log(message); // "Hello"
}
Your code: How JS sees it:
┌─────────────────────┐ ┌─────────────────────┐
│ function foo() { │ │ function foo() { │
│ │ │ var x; // hoisted│
│ console.log(x); │ ──► │ console.log(x); │
│ var x = 5; │ │ x = 5; │
│ } │ │ } │
└─────────────────────┘ └─────────────────────┘
Variables declared with let and const are block-scoped. A block is any code within curly braces {}: if statements, for loops, while loops, or just standalone blocks.
if (true) {
let blockLet = "I'm block-scoped";
const blockConst = "Me too";
var functionVar = "I escape the block!";
}
// console.log(blockLet); // ✗ ReferenceError
// console.log(blockConst); // ✗ ReferenceError
console.log(functionVar); // ✓ "I escape the block!"
Unlike var, variables declared with let and const are not initialized until their declaration is evaluated. Accessing them before declaration causes a ReferenceError. This period is called the Temporal Dead Zone.
function demo() {
// TDZ for 'name' starts here
console.log(name); // ReferenceError: Cannot access 'name' before initialization
let name = "Alice"; // TDZ ends here
console.log(name); // "Alice"
}
┌────────────────────────────────────────────────────────────┐
│ │
│ function demo() { │
│ │
│ ┌────────────────────────────────────────────────┐ │
│ │ TEMPORAL DEAD ZONE │ │
│ │ │ │
│ │ 'name' exists but cannot be accessed yet! │ │
│ │ │ │
│ │ console.log(name); // ReferenceError │ │
│ │ │ │
│ └────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ let name = "Alice"; // TDZ ends here │
│ │
│ console.log(name); // "Alice" - works fine! │
│ │
│ } │
│ │
└────────────────────────────────────────────────────────────┘
Here's a comprehensive comparison of the three variable declaration keywords:
| Feature | var | let | const |
|---|---|---|---|
| Scope | Function | Block | Block |
| Hoisting | Yes (initialized as undefined) | Yes (but TDZ) | Yes (but TDZ) |
| Redeclaration | ✓ Allowed | ✗ Error | ✗ Error |
| Reassignment | ✓ Allowed | ✓ Allowed | ✗ Error |
| Must Initialize | No | No | Yes |
// let and const prevent redeclaration
let age = 25
// let age = 30 // SyntaxError: 'age' has already been declared
const PI = 3.14
// const PI = 3.14159 // SyntaxError
```
let score = 100;
score = 200; // ✓ Fine
// const prevents reassignment
const API_KEY = "abc123"
// API_KEY = "xyz789" // TypeError: Assignment to constant variable
// BUT: const objects/arrays CAN be mutated!
const user = { name: "Alice" }
user.name = "Bob" // ✓ This works!
user.age = 25 // ✓ This works too!
// user = {} // ✗ This fails (reassignment)
```
// let: hoisted but NOT initialized (TDZ)
// console.log(b); // ReferenceError!
let b = 2;
// const: same as let
// console.log(c); // ReferenceError!
const c = 3;
}
```
This is one of the most common JavaScript gotchas, and it perfectly illustrates why let is preferred over var:
// The Problem: var is function-scoped
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i)
}, 100)
}
// Output: 3, 3, 3 (not 0, 1, 2!)
// Why? There's only ONE 'i' variable shared across all iterations.
// By the time the setTimeout callbacks run, the loop has finished and i === 3.
The setTimeout() callbacks all close over the same i variable, which equals 3 by the time they execute. (To understand why the callbacks don't run immediately, see our Event Loop guide.)
// The Solution: let is block-scoped
for (let i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i)
}, 100)
}
// Output: 0, 1, 2 (correct!)
// Why? Each iteration gets its OWN 'i' variable.
// Each setTimeout callback closes over a different 'i'.
This approach catches bugs at compile time and makes your intent clear. </Tip>
Lexical scope (also called static scope) means that the scope of a variable is determined by its position in the source code, not by how functions are called at runtime. As Kyle Simpson explains in You Don't Know JS: Scope & Closures, lexical scope is determined at "lex-time" — the time when the code is being parsed — which is why it is also called "static" scope.
const outer = "I'm outside!";
function outerFunction() {
const middle = "I'm in the middle!";
function innerFunction() {
const inner = "I'm inside!";
// innerFunction can access all three variables
console.log(inner); // ✓ Own scope
console.log(middle); // ✓ Parent scope
console.log(outer); // ✓ Global scope
}
innerFunction();
// console.log(inner); // ✗ ReferenceError
}
outerFunction();
// console.log(middle); // ✗ ReferenceError
When JavaScript needs to find a variable, it walks up the scope chain. It starts from the current scope and moves outward until it finds the variable or reaches the global scope.
<Steps> <Step title="Look in Current Scope"> JavaScript first checks if the variable exists in the current function/block scope. </Step> <Step title="Look in Parent Scope"> If not found, it checks the enclosing (parent) scope. </Step> <Step title="Continue Up the Chain"> This process continues up through all ancestor scopes. </Step> <Step title="Reach Global Scope"> Finally, it checks the global scope. If still not found, a `ReferenceError` is thrown. </Step> </Steps>Variable Lookup: Where is 'x'?
┌─────────────────────────────────────────────────┐
│ Global Scope │
│ x = "global" │
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ outer() Scope │ │
│ │ x = "outer" │ │
│ │ │ │
│ │ ┌─────────────────────────────────┐ │ │
│ │ │ inner() Scope │ │ │
│ │ │ │ │ │
│ │ │ console.log(x); │ │ │
│ │ │ │ │ │ │
│ │ │ ▼ │ │ │
│ │ │ 1. Check inner() → not found │ │ │
│ │ │ │ │ │ │
│ │ └─────────│───────────────────────┘ │ │
│ │ ▼ │ │
│ │ 2. Check outer() → FOUND! "outer" │ │
│ │ │ │
│ └─────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────┘
Result: "outer"
When an inner scope declares a variable with the same name as an outer scope, the inner variable "shadows" the outer one:
const name = "Global";
function greet() {
const name = "Function"; // Shadows global 'name'
if (true) {
const name = "Block"; // Shadows function 'name'
console.log(name); // "Block"
}
console.log(name); // "Function"
}
greet();
console.log(name); // "Global"
A closure is the combination of a function bundled together with references to its surrounding state (the lexical environment). According to MDN, a closure gives a function access to variables from an outer (enclosing) scope, even after that outer function has finished executing and returned. Every function in JavaScript creates a closure at creation time.
Remember our office building analogy? A closure is like someone who worked in the private office, left the building, but still remembers exactly where everything was, and can still use that knowledge!
In JavaScript, closures are created automatically every time you create a function. The function maintains a reference to its lexical environment.
function createGreeter(greeting) {
// 'greeting' is in createGreeter's scope
return function(name) {
// This inner function is a closure!
// It "closes over" the 'greeting' variable
console.log(`${greeting}, ${name}!`);
};
}
const sayHello = createGreeter("Hello");
const sayHola = createGreeter("Hola");
// createGreeter has finished executing, but...
sayHello("Alice"); // "Hello, Alice!"
sayHola("Bob"); // "Hola, Bob!"
// The inner functions still remember their 'greeting' values!
After createGreeter("Hello") returns:
┌──────────────────────────────────────┐
│ sayHello (Function) │
├──────────────────────────────────────┤
│ [[Code]]: function(name) {...} │
│ │
│ [[Environment]]: ────────────────────────┐
└──────────────────────────────────────┘ │
▼
┌────────────────────────────┐
│ Lexical Environment │
│ (Kept alive by closure!) │
├────────────────────────────┤
│ greeting: "Hello" │
└────────────────────────────┘
Closures aren't just a theoretical concept. You'll use them every day. Here are the patterns that make closures so powerful.
Closures let you create truly private variables in JavaScript:
function createCounter() {
let count = 0; // Private variable - no way to access directly!
return {
increment() {
count++;
return count;
},
decrement() {
count--;
return count;
},
getCount() {
return count;
}
};
}
const counter = createCounter();
console.log(counter.getCount()); // 0
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.decrement()); // 1
// There's NO way to access 'count' directly!
console.log(counter.count); // undefined
Closures let you create specialized functions on the fly:
function createMultiplier(multiplier) {
return function(number) {
return number * multiplier;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
const tenX = createMultiplier(10);
console.log(double(5)); // 10
console.log(triple(5)); // 15
console.log(tenX(5)); // 50
// Each function "remembers" its own multiplier
This pattern works great with the Fetch API for creating reusable API clients:
// Real-world example: API request factories
function createApiClient(baseUrl) {
return {
get(endpoint) {
return fetch(`${baseUrl}${endpoint}`);
},
post(endpoint, data) {
return fetch(`${baseUrl}${endpoint}`, {
method: 'POST',
body: JSON.stringify(data)
});
}
};
}
const githubApi = createApiClient('https://api.github.com');
const myApi = createApiClient('https://myapp.com/api');
// Each client remembers its baseUrl
githubApi.get('/users/leonardomso');
myApi.get('/users/1');
Closures are essential for maintaining state in asynchronous code. When you use addEventListener() to attach event handlers, those handlers can close over variables from their outer scope:
function setupClickCounter(buttonId) {
let clicks = 0; // This variable persists across clicks!
const button = document.getElementById(buttonId);
button.addEventListener('click', function() {
clicks++;
console.log(`Button clicked ${clicks} time${clicks === 1 ? '' : 's'}`);
});
}
setupClickCounter('myButton');
// Each click increments the same 'clicks' variable
// Click 1: "Button clicked 1 time"
// Click 2: "Button clicked 2 times"
// Click 3: "Button clicked 3 times"
Closures enable efficient caching of expensive computations:
function createMemoizedFunction(fn) {
const cache = {}; // Cache persists across calls!
return function(arg) {
if (arg in cache) {
console.log('Returning cached result');
return cache[arg];
}
console.log('Computing result');
const result = fn(arg);
cache[arg] = result;
return result;
};
}
// Expensive operation: calculate factorial
function factorial(n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
const memoizedFactorial = createMemoizedFunction(factorial);
console.log(memoizedFactorial(5)); // Computing result → 120
console.log(memoizedFactorial(5)); // Returning cached result → 120
console.log(memoizedFactorial(5)); // Returning cached result → 120
Understanding scope and closures means understanding where things go wrong. These are the mistakes that trip up even experienced developers.
This is the classic closure trap. Almost everyone gets it wrong the first time:
// What does this print?
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
// Most people expect: 0, 1, 2
// Actual output: 3, 3, 3
What actually happens:
TIME ════════════════════════════════════════════════════►
┌─────────────────────────────────────────────────────────┐
│ IMMEDIATELY (milliseconds): │
│ │
│ Loop iteration 1: i = 0, schedule callback │
│ Loop iteration 2: i = 1, schedule callback │
│ Loop iteration 3: i = 2, schedule callback │
│ Loop ends: i = 3 │
│ │
│ All 3 callbacks point to the SAME 'i' variable ──┐ │
└─────────────────────────────────────────────────────│───┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ ~1 SECOND LATER: │
│ │
│ callback1 runs: "What's i?" → i is 3 → prints 3 │
│ callback2 runs: "What's i?" → i is 3 → prints 3 │
│ callback3 runs: "What's i?" → i is 3 → prints 3 │
│ │
└─────────────────────────────────────────────────────────┘
Result: 3, 3, 3 (not 0, 1, 2!)
```javascript
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
// Output: 0, 1, 2 ✓
```
```javascript
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(function() {
console.log(j);
}, 1000);
})(i); // Pass i as argument, creating a new 'j' each time
}
// Output: 0, 1, 2 ✓
```
```javascript
[0, 1, 2].forEach(function(i) {
setTimeout(function() {
console.log(i);
}, 1000);
});
// Output: 0, 1, 2 ✓
```
Closures are powerful, but they come with responsibility. Since closures keep references to their outer scope variables, those variables can't be garbage collected.
function createHeavyClosure() {
const hugeData = new Array(1000000).fill('x'); // Large data
return function() {
// This reference to hugeData keeps the entire array in memory
console.log(hugeData.length);
};
}
const leakyFunction = createHeavyClosure();
// hugeData is still in memory because the closure references it
When you're done with a closure, explicitly break the reference. Use removeEventListener() to clean up event handlers:
function setupHandler(element) {
// Imagine this returns a large dataset
const largeData = { users: new Array(10000).fill({ name: 'User' }) };
const handler = function() {
console.log(`Processing ${largeData.users.length} users`);
};
element.addEventListener('click', handler);
// Return a cleanup function
return function cleanup() {
element.removeEventListener('click', handler);
// Now handler and largeData can be garbage collected
};
}
const button = document.getElementById('myButton');
const cleanup = setupHandler(button);
// Later, when you're done with this functionality:
cleanup(); // Removes listener, allows memory to be freed
Scope = Variable Visibility — It determines where variables can be accessed
Three types of scope: Global (everywhere), Function (var), Block (let/const)
Lexical scope is static — Determined by code position, not runtime behavior
Scope chain — JavaScript looks up variables from inner to outer scope
let and const are block-scoped — Prefer them over var
Temporal Dead Zone — let/const can't be accessed before declaration
Closure = Function + Its Lexical Environment — Functions "remember" where they were created
Closures enable: Data privacy, function factories, stateful callbacks, memoization
Watch for the loop gotcha — Use let instead of var in loops with async callbacks
Mind memory — Closures keep references alive; clean up when done
</Info>1. **Global Scope** — Variables declared outside any function or block; accessible everywhere
2. **Function Scope** — Variables declared with `var` inside a function; accessible only within that function
3. **Block Scope** — Variables declared with `let` or `const` inside a block `{}`; accessible only within that block
```javascript
const global = "everywhere"; // Global scope
function example() {
var functionScoped = "function"; // Function scope
if (true) {
let blockScoped = "block"; // Block scope
}
}
```
```javascript
function example() {
// TDZ starts for 'x'
console.log(x); // ReferenceError!
// TDZ continues...
let x = 10; // TDZ ends
console.log(x); // 10 ✓
}
```
The TDZ helps catch bugs where you accidentally use variables before they're initialized.
Inner functions have access to variables declared in their outer functions because of where they are written, not because of when they're invoked.
```javascript
function outer() {
const message = "Hello";
function inner() {
console.log(message); // Can access 'message' because of lexical scope
}
return inner;
}
const fn = outer();
fn(); // "Hello" — still works even though outer() has returned
```
```javascript
function createCounter() {
let count = 0; // This variable is "enclosed" in the closure
return function() {
count++;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
// 'count' persists because of the closure
```
Every function in JavaScript creates a closure. The term usually refers to situations where this behavior is notably useful or surprising.
**Answer:** It outputs `3, 3, 3`.
**Why?** Because `var` is function-scoped (not block-scoped), there's only ONE `i` variable shared across all iterations. By the time the `setTimeout` callbacks execute (after ~100ms), the loop has already completed and `i` equals `3`.
**Fix:** Use `let` instead of `var`:
```javascript
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Outputs: 0, 1, 2
```
With `let`, each iteration gets its own `i` variable, and each callback closes over a different value.
1. **Data Privacy** — Creating private variables that can't be accessed directly:
```javascript
function createWallet(initial) {
let balance = initial;
return {
spend(amount) { balance -= amount; },
getBalance() { return balance; }
};
}
```
2. **Function Factories** — Creating specialized functions:
```javascript
function createTaxCalculator(rate) {
return (amount) => amount * rate;
}
const calculateVAT = createTaxCalculator(0.20);
```
3. **Maintaining State in Callbacks** — Event handlers, timers, API calls:
```javascript
function setupLogger(prefix) {
return (message) => console.log(`[${prefix}] ${message}`);
}
```
4. **Memoization/Caching** — Storing computed results:
```javascript
function memoize(fn) {
const cache = {};
return (arg) => cache[arg] ?? (cache[arg] = fn(arg));
}
```
```javascript
function makeGreeter(name) {
return function greet() {
return `Hi, ${name}`;
};
}
```