Understanding JavaScript Block Scope with let and const

Block scope is a fundamental JavaScript mechanism introduced in ECMAScript 2015 (ES6) that restricts the visibility and accessibility of variables declared with let and const to the specific block of code in which they are defined. Unlike variables declared with var, which are scoped to the containing function or global execution context, let and const ensure that variables exist only within the curly braces ({}) enclosing them. This article explains how block scopes work, how they handle hoisting and the Temporal Dead Zone, and how they prevent common scope-related bugs in modern JavaScript development.


What Defines a Block?

In JavaScript, a block is any code section delimited by a pair of opening and closing curly braces ({ ... }). Common examples of blocks include:

Any variable declared inside these braces using let or const cannot be accessed from outside that block.

{
  let insideBlock = "Visible only here";
  const alsoInside = 42;
  console.log(insideBlock); // Output: Visible only here
}

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

How let and const Function in Block Scope

1. Inaccessibility Outside the Block

When the JavaScript engine enters a block, it allocates memory for block-scoped variables. Once the engine exits the block, those variables are cleaned up and removed from the active lexical environment, making them completely unreachable from the parent scope.

2. Variable Shadowing

A variable declared with let or const inside an inner block can have the same name as a variable in an outer block. The inner variable “shadows” the outer one without overwriting it:

let count = 10;

if (true) {
  let count = 20; // Shadows outer 'count'
  console.log(count); // Output: 20
}

console.log(count); // Output: 10

3. No Re-declaration in the Same Scope

Within the same block scope, you cannot re-declare a variable that was declared with let or const. Doing so results in a SyntaxError.

let user = "Alice";
let user = "Bob"; // SyntaxError: Identifier 'user' has already been declared

However, re-declaring the same variable name in a separate or nested block is valid because each block creates a new lexical scope.


Hoisting and the Temporal Dead Zone (TDZ)

Like var, declarations using let and const are hoisted to the top of their containing block. However, they are not initialized with undefined.

The time between entering the scope and the actual line of code where the variable is declared and initialized is called the Temporal Dead Zone (TDZ). Accessing a let or const variable within the TDZ results in a ReferenceError.

{
  // TDZ for 'message' begins
  console.log(message); // ReferenceError: Cannot access 'message' before initialization
  
  let message = "Hello"; // TDZ ends; variable is initialized
  console.log(message); // Output: Hello
}

Block Scope in Loops

One of the most practical benefits of block scoping is its behavior inside loops. When using let in a for loop, a new lexical binding is created for every single iteration:

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

If var were used instead, only a single global or function-scoped i would exist, causing the timeout callback to output 3 three times.


Difference Between let and const Inside Blocks

While both create block-scoped identifiers, they differ in re-assignment: