Skip to main content

What is scope?

Scope is the set of rules that determines where a variable can be accessed. When you declare a variable, it’s only visible within a certain region of your code. Try to use it outside that region and you’ll get an error.
function processOrder() {
  const orderId = "ORD-001";
  console.log(orderId); // ✅ Works — same scope
}

processOrder();
console.log(orderId); // ❌ ReferenceError: orderId is not defined
orderId exists inside processOrder and nowhere else. This is a good thing — it means variables don’t leak into places they shouldn’t be.
Python mental model: this is the same general idea as Python scope (LEGB: Local, Enclosing, Global, Built-in). The biggest difference you’ll feel in JavaScript is that let/const also create block scope for if/for/while.

Block scope

Variables declared with const and let are block-scoped. They exist only within the nearest set of curly braces {}.
if (true) {
  const message = "Inside the block";
  let count = 42;
  console.log(message); // ✅ "Inside the block"
  console.log(count);   // ✅ 42
}

console.log(message); // ❌ ReferenceError
console.log(count);   // ❌ ReferenceError
Every if, for, while, and function body creates a new block scope:
for (let i = 0; i < 3; i++) {
  const item = `Item ${i}`;
  console.log(item); // ✅ Works inside the loop
}

console.log(i);    // ❌ ReferenceError
console.log(item); // ❌ ReferenceError
// Block-scoped — each block is its own world
if (true) {
  const x = 10;
}
console.log(x); // ReferenceError
This is a real difference from Python. In Python, if and for blocks don’t create their own scope — variables leak out. In JavaScript, they don’t.
If you’re used to Python, this will trip you up. Variables declared inside if, for, or while blocks in JavaScript are not accessible outside those blocks. This is actually safer — it prevents accidental name collisions.

Function scope

Functions create their own scope. Variables declared inside a function are invisible outside it.
function calculateDiscount(price) {
  const discountRate = 0.15;
  const discount = price * discountRate;
  return price - discount;
}

console.log(calculateDiscount(100)); // 85
console.log(discountRate); // ❌ ReferenceError
console.log(discount);     // ❌ ReferenceError
Nested functions can access variables from their parent function:
function outer() {
  const outerVar = "I'm from outer";

  function inner() {
    const innerVar = "I'm from inner";
    console.log(outerVar); // ✅ Can see parent's variables
    console.log(innerVar); // ✅ Can see own variables
  }

  inner();
  console.log(innerVar); // ❌ Can't see child's variables
}
This is called lexical scoping — inner functions can look “up” to their parent’s scope, but parents can’t look “down” into children.
This part is very similar to Python: nested functions can read variables from outer functions. That’s the foundation for closures in both languages.

Global scope

Variables declared outside any function or block are in the global scope. They’re accessible everywhere.
const API_URL = "http://localhost:8000"; // Global

function fetchData() {
  console.log(API_URL); // ✅ Works — global is visible everywhere
}

function sendData() {
  console.log(API_URL); // ✅ Also works
}
Keep global variables to a minimum. Use them for true constants like API URLs and configuration values. Everything else should live in the smallest scope possible.

Scope chain

When JavaScript encounters a variable, it searches from the current scope outward:
const color = "blue"; // Global

function paintRoom() {
  const color = "green"; // Function scope — shadows the global

  if (true) {
    const color = "red"; // Block scope — shadows the function scope
    console.log(color);  // "red"
  }

  console.log(color); // "green"
}

paintRoom();
console.log(color); // "blue"
Each color is a different variable in a different scope. The inner-most scope wins. This is called variable shadowing.
Python mental model: same idea as name shadowing in nested scopes — a local variable named color hides an outer color. JavaScript’s scope chain and Python’s LEGB rule are solving the same problem.
Variable shadowing is legal but can be confusing. Avoid reusing the same variable name in nested scopes. ESLint can warn you about this with the no-shadow rule.

Common mistakes

// ❌ Wrong: Variable only exists inside the if block
if (userIsLoggedIn) {
  const welcomeMessage = `Welcome back, ${userName}!`;
}
console.log(welcomeMessage); // ReferenceError

// ✅ Correct: Declare it in the outer scope
let welcomeMessage = "";
if (userIsLoggedIn) {
  welcomeMessage = `Welcome back, ${userName}!`;
}
console.log(welcomeMessage); // Works
If you need a variable after a block ends, declare it before the block with let. You can assign the value inside the block.
// var is hoisted and function-scoped — avoid it
console.log(name); // undefined (not an error!)
var name = "Sarah";

// const and let are NOT hoisted the same way
console.log(name); // ReferenceError: Cannot access before initialization
const name = "Sarah";
var has confusing hoisting behavior — it exists before its declaration but with the value undefined. This is one of the main reasons the JavaScript community moved to const and let. Stick with const and let and you’ll never deal with hoisting issues.

What’s next?

You understand how scope works — variables live inside blocks and functions, and inner scopes can see outer scopes. This leads directly to one of JavaScript’s most useful features: closures.

Closures

How functions remember their surrounding variables