JavaScript Scopes

javascript

let language = "JavaScript";

function makeCounter() {
  let count = 0; // outer function scope

  return function next() { // closure keeps count reachable
    return ++count;
  };
}

const nextCount = makeCounter();
console.log(nextCount()); // 1
console.log(nextCount()); // 2

{
  // console.log(value); // ReferenceError: value is in the TDZ
  let value = "block value";
  console.log(value); // works after declaration
}

The temporal dead zone is about when a binding may be accessed. A closure is about retaining access to an outer lexical environment, not copying its values into the inner function.

When debugging: Where was the function written? Which scope declares the name? Has the let or const declaration run? Is a returned callback keeping outer data reachable?

Scope determines where a name can be accessed in JavaScript. Code can use bindings from its current scope and, when one is not found, from enclosing scopes. It cannot reach a binding that belongs only to a nested scope. The exact rules also depend on how the binding was declared, because var, let, and const do not have identical scope and initialization behavior.

Scope is what keeps a variable local to the code that needs it. It reduces naming conflicts, limits accidental changes, and makes it easier to reason about which value a name refers to.

The main scopes you will work with are global scope, function scope, block scope, and, in modules, module scope. The rest of this tutorial shows how JavaScript finds a name and how each scope behaves.

JavaScript what is scope

javascript

// 'name' is accessible here because it is in the global scope
let name = "JavaScript";

function greet() {
  // 'name' is accessible inside this function too
  console.log("Hello, " + name);
}

greet(); // Output: Hello, JavaScript

Scoping is the process JavaScript uses to resolve a name to a binding. When code runs, the engine searches nested lexical environments using consistent rules.

JavaScript checks the current scope first, then moves outward one level at a time. If it cannot find the name, it throws a ReferenceError. If an inner scope declares the same name, that binding shadows the outer one while the inner scope is active.

JavaScript what is scoping

javascript

let language = "JavaScript";

function outer() {
  let framework = "React";

  function inner() {
    // 'inner' can access 'framework' from outer() and 'language' from global scope
    console.log(language);  // Output: JavaScript
    console.log(framework); // Output: React
  }

  inner();
}

outer();

JavaScript uses lexical scoping, also called static scoping. The place where you write a function determines which outer bindings it can access, not the place where you call it.

A function can reach bindings in its own scope and in the parent scopes that surround its definition. This scope chain is the foundation of closures.

JavaScript lexical scoping

javascript

function outerFunction() {
  let outerVar = "I am from outer function";

  function innerFunction() {
    // innerFunction can access outerVar because of lexical scoping
    console.log(outerVar);
  }

  return innerFunction;
}

const myFunc = outerFunction();
myFunc(); // Output: I am from outer function

Notice that innerFunction can still reach outerVar even after outerFunction has finished running. Lexical scoping remembers the scope where the function was originally written, not where it is called.

JavaScript has three common types of scope, plus module scope when you use import and export:

  • Global Scope: Variables declared outside any function or block.
  • Function Scope: The scope created by a function. Variables declared with var belong to the nearest function; let and const also stay inside the function unless a smaller block limits them further.
  • Block Scope: Variables declared inside a block ({}) using let or const.
JavaScript different types of scope

JavaScript modules also have their own module scope. Top-level bindings in one module do not automatically become global or appear in another module.

The next sections look at each scope in detail.

A binding has global scope when it is available throughout the relevant script or application environment. In a classic browser script, a top-level declaration can be global. In an ES module, top-level declarations belong to the module scope instead and are not automatically global. Code in nested functions and blocks can read a global binding unless a nearer binding with the same name shadows it.

A small number of deliberate globals can be useful for shared configuration or browser APIs. Too many globals let unrelated code change the same state, which creates naming conflicts and makes a growing program harder to debug.

Keep application globals to a minimum. Prefer a module, function, or block binding where possible so the value's ownership and lifetime are clear.

JavaScript global scope

javascript

// Global variable
let color = "blue";

function printColor() {
  console.log(color); // Output: blue
}

printColor();
console.log(color); // Output: blue

Global variables are handy for sharing data, but too many can cause naming conflicts and unexpected changes. Keep shared values deliberate and localize everything else.

A function creates its own function scope. Code inside the function can use its local bindings, but code outside cannot access them directly. A nested function can read bindings from its containing function through the scope chain.

Values created inside a function stay private unless the function returns a value or exposes a function that uses them. Returning a value sends a result out, while returning an inner function can preserve access through a closure.

Function scope limits where a binding can be changed and makes its purpose easier to understand. If a value is needed only inside one function, declare it there.

JavaScript functional scope

javascript

function showMessage() {
  let message = "Hello from inside the function!";
  console.log(message); // Output: Hello from inside the function!
}

showMessage();
console.log(message); // ReferenceError: message is not defined

Inside a function, var is function-scoped, while let and const are block-scoped. In a simple function body without inner blocks, all three are available throughout that function.

Nested functions can access variables from their parent function. When an inner function keeps using those variables after the outer function has finished, this relationship is called a closure:

javascript

function outer() {
  let x = 10;

  function inner() {
    let y = 5;
    console.log(x + y); // Output: 15
  }

  inner();
  console.log(y); // ReferenceError: y is not defined
}

outer();

Block scope applies to bindings declared with let and const. A block is code wrapped in curly braces {}, such as an if statement, a for loop, or a while loop.

Variables declared with let or const inside a block stay inside that block. var ignores block boundaries and is function-scoped instead. A let or const binding is also in the temporal dead zone from the start of its block until its declaration is evaluated.

JavaScript block scope

javascript

if (true) {
  let a = 10;
  const b = 20;
  var c = 30;
}

console.log(c); // Output: 30 (var is NOT block-scoped)
console.log(a); // ReferenceError: a is not defined
console.log(b); // ReferenceError: b is not defined

Here is another example using a for loop:

javascript

for (let i = 0; i < 3; i++) {
  console.log(i); // Output: 0, 1, 2
}

console.log(i); // ReferenceError: i is not defined

// Compare with var
for (var j = 0; j < 3; j++) {
  console.log(j); // Output: 0, 1, 2
}

console.log(j); // Output: 3 (var is NOT block-scoped)

In loops, let and const stay inside the loop block, but var is function-scoped and can still be accessed after the loop ends. A let loop variable gets a separate binding for each iteration, which is useful for callbacks that run later.

Stick with let and const over var whenever you can. Block scoping keeps your code predictable and much easier to debug.

Tip: If callbacks in a loop all log the same value, check if you used var. In most cases, let is the better choice.

Best practice: Prefer let and const for predictable block scope. Use var only when you are dealing with legacy code.

  • Scope determines where a name can be resolved and which bindings the code can access.
  • Scoping is the process the engine uses to look up variables.
  • Lexical scoping means scope is determined by where the code is written, not where it is called.
  • Global scope: bindings available across the relevant script or environment. In modules, top-level bindings belong to module scope.
  • Function scope: the scope created by a function. A nested function can access parent bindings through the scope chain and closures.
  • Block scope: bindings declared with let or const inside a block ({}), accessible only within that block.
  • var is function-scoped, while let and const are block-scoped. Prefer let and const in modern JavaScript.

Next step: Apply these scope rules while learning how to define and reuse code in the next tutorial on functions.

Videos for this topic will be added soon.

Reviewed by

SimplyJavaScript Editorial Team

Technical editors and JavaScript educators with hands-on experience building frontend projects, writing learning material, and reviewing tutorials for clarity, accuracy, and beginner-friendly guidance.