let and const Block Scope in JavaScript

Block Scope Basics

Block scope defines the accessibility of variables within a block of code (enclosed by curly braces {}). Before ES6, JavaScript only had function scope and global scope, but let and const introduced true block scoping.

var vs let vs const

var

  • Function-scoped or globally-scoped
  • Hoisted (declaration moved to the top)
  • Can be redeclared
  • Can be updated
function varExample() {
  var x = 1;
  
  if (true) {
    var x = 2; // Same variable, redeclared
    console.log(x); // 2
  }
  
  console.log(x); // 2 (value changed)
}

let

  • Block-scoped
  • Hoisted but not initialized (Temporal Dead Zone)
  • Cannot be redeclared in same scope
  • Can be updated
function letExample() {
  let x = 1;
  
  if (true) {
    let x = 2; // Different variable, scoped to the block
    console.log(x); // 2
  }
  
  console.log(x); // 1 (unchanged)
}

const

  • Block-scoped
  • Hoisted but not initialized (Temporal Dead Zone)
  • Cannot be redeclared in same scope
  • Cannot be reassigned
  • Object properties can still be modified
function constExample() {
  const x = 1;
  
  if (true) {
    const x = 2; // Different variable, scoped to the block
    console.log(x); // 2
  }
  
  console.log(x); // 1 (unchanged)
  
  // x = 3; // TypeError: Assignment to constant variable
}

Block Scope Examples

let in Blocks

// Block scope with let
{
  let blockScoped = 'I am block-scoped';
  console.log(blockScoped); // 'I am block-scoped'
}
// console.log(blockScoped); // ReferenceError: blockScoped is not defined

// let in if statements
if (true) {
  let ifScoped = 'Only available in this if block';
  console.log(ifScoped); // 'Only available in this if block'
}
// console.log(ifScoped); // ReferenceError: ifScoped is not defined

// let in for loops
for (let i = 0; i < 3; i++) {
  console.log(i); // 0, 1, 2
}
// console.log(i); // ReferenceError: i is not defined

const in Blocks

// Block scope with const
{
  const PI = 3.14159;
  console.log(PI); // 3.14159
}
// console.log(PI); // ReferenceError: PI is not defined

// const with objects
{
  const user = { name: 'John', age: 30 };
  console.log(user.name); // 'John'
  
  // Can modify properties
  user.name = 'Jane';
  console.log(user.name); // 'Jane'
  
  // Cannot reassign the variable
  // user = { name: 'Bob' }; // TypeError: Assignment to constant variable
}

Temporal Dead Zone (TDZ)

The TDZ is the period between entering a scope where a variable is declared and the actual declaration.

// Temporal Dead Zone demonstration
function tdz() {
  // TDZ for x starts here
  console.log(y); // undefined (var is hoisted with initialization)
  // console.log(x); // ReferenceError: Cannot access 'x' before initialization
  
  var y = 1; // y is hoisted
  let x = 2; // TDZ ends for x
  
  console.log(x); // 2
  console.log(y); // 1
}

Function Scope vs Block Scope

// Function scope with var
function functionScope() {
  var functionScoped = 'I am function-scoped';
  
  if (true) {
    var alsoFunctionScoped = 'I am also function-scoped';
  }
  
  console.log(functionScoped);      // 'I am function-scoped'
  console.log(alsoFunctionScoped);  // 'I am also function-scoped'
}

// Block scope with let/const
function blockScope() {
  let blockScoped = 'I am block-scoped';
  
  if (true) {
    let onlyInIf = 'I am only available in this if block';
    const alsoOnlyInIf = 'I am also only in this if block';
    
    console.log(blockScoped);       // 'I am block-scoped' (accessible from parent scope)
    console.log(onlyInIf);          // 'I am only available in this if block'
    console.log(alsoOnlyInIf);      // 'I am also only in this if block'
  }
  
  console.log(blockScoped);         // 'I am block-scoped'
  // console.log(onlyInIf);         // ReferenceError: onlyInIf is not defined
  // console.log(alsoOnlyInIf);     // ReferenceError: alsoOnlyInIf is not defined
}

Loop Scope with let

// var in loops (problematic)
function varLoop() {
  const actions = [];
  
  for (var i = 0; i < 3; i++) {
    actions.push(function() {
      console.log(i);
    });
  }
  
  actions.forEach(action => action());
  // Logs: 3, 3, 3 (all functions reference the same variable)
}

// let in loops (correct)
function letLoop() {
  const actions = [];
  
  for (let i = 0; i < 3; i++) {
    actions.push(function() {
      console.log(i);
    });
  }
  
  actions.forEach(action => action());
  // Logs: 0, 1, 2 (each iteration gets its own i)
}

Global Variables with let and const

// var creates properties on the global object
var globalVar = 'I am a global var';
console.log(window.globalVar); // 'I am a global var' (in browsers)

// let and const do not create properties on the global object
let globalLet = 'I am a global let';
const globalConst = 'I am a global const';
console.log(window.globalLet); // undefined
console.log(window.globalConst); // undefined

Redeclaration Rules

// var can be redeclared
var x = 1;
var x = 2; // Valid
console.log(x); // 2

// let cannot be redeclared in the same scope
let y = 1;
// let y = 2; // SyntaxError: Identifier 'y' has already been declared

// const cannot be redeclared in the same scope
const z = 1;
// const z = 2; // SyntaxError: Identifier 'z' has already been declared

// Different scopes allow redeclaration
function scopeTest() {
  let scopedVar = 'outer';
  console.log(scopedVar); // 'outer'
  
  if (true) {
    let scopedVar = 'inner'; // Valid - different scope
    console.log(scopedVar); // 'inner'
  }
  
  console.log(scopedVar); // 'outer'
}

Best Practices

When to Use let

  • For variables that will be reassigned
  • Loop counters and iterators
  • Variables that change over time
let count = 0;
count++; // Valid

let currentUser;
if (isLoggedIn) {
  currentUser = getUser();
}

When to Use const

  • For values that should not be reassigned
  • Most variable declarations (prefer const by default)
  • Object and array declarations (even if their contents will change)
const API_URL = 'https://api.example.com';
const MAX_ITEMS = 100;

const user = { name: 'John' };
user.name = 'Jane'; // Valid - modifying properties

const numbers = [1, 2, 3];
numbers.push(4); // Valid - modifying array
// numbers = [5, 6]; // Invalid - reassigning variable

Avoiding var

In modern JavaScript, it’s generally recommended to avoid var and use let and const instead:

  • var doesn’t respect block scope
  • var can lead to unexpected behavior due to hoisting
  • var allows redeclaration, which can lead to bugs

Common Patterns

Immediate Block Scope

// Create a temporary scope
{
  const temporary = performHeavyComputation();
  processSomething(temporary);
  // temporary is not accessible outside this block
}
// No memory leak or pollution of outer scope

Switch Case Blocks

switch (value) {
  case 1: {
    // Block scope for case 1
    const result = 'one';
    console.log(result);
    break;
  }
  case 2: {
    // Different block scope for case 2
    const result = 'two'; // No conflict with case 1's result
    console.log(result);
    break;
  }
}

Module Pattern with Block Scope

const counter = (function() {
  // Private variables in module scope
  let count = 0;
  
  return {
    increment() {
      return ++count;
    },
    decrement() {
      return --count;
    },
    getCount() {
      return count;
    }
  };
})();

console.log(counter.getCount()); // 0
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.decrement()); // 1
// console.log(count); // ReferenceError: count is not defined

Interview Tips

  • Explain the difference between function scope and block scope
  • Describe the Temporal Dead Zone and how it affects let and const
  • Explain why let is preferred over var in modern JavaScript
  • Discuss when to use const vs let
  • Explain how let in loops creates a new binding for each iteration
  • Describe how const prevents reassignment but not mutation of objects
  • Explain the behavior of let, const, and var in the global scope

Test Your Knowledge

Take a quick quiz to test your understanding of this topic.