JavaScriptScopeClosuresFrontend

JavaScript var vs let: Function Scope, Block Scope, and Closures

Understand the difference between var and let in JavaScript through function scope, block scope, for loops, closures, and why let avoids the classic 3 3 3 result.

·Updated ·8 min read·Counting...
JavaScript var vs let: Function Scope, Block Scope, and Closures

var and let both declare variables, but their scope behavior is very different. var is function-scoped. let is block-scoped. That difference matters most in loops, closures, callbacks, and code that expects variables to disappear after an if or for block.

Modern JavaScript should usually prefer const first and let when reassignment is needed. Use var mainly when reading or maintaining older code.

Function scope with var

var is scoped to the nearest function, not the nearest block:

function example() {
  if (true) {
    var x = 10
  }

  console.log(x) // 10
}

example()

Even though x is declared inside the if block, it is available throughout the function.

Block scope with let

let is scoped to the nearest block:

function example() {
  if (true) {
    let x = 10
  }

  console.log(x) // ReferenceError
}

example()

The variable exists only inside the if block.

The classic loop closure problem

With var:

function createCounters() {
  const counters = []

  for (var i = 0; i < 3; i++) {
    counters.push(function () {
      return i
    })
  }

  return counters
}

const counters = createCounters()
console.log(counters[0]()) // 3
console.log(counters[1]()) // 3
console.log(counters[2]()) // 3

All three functions share the same i. After the loop finishes, i is 3.

With let:

function createCounters() {
  const counters = []

  for (let i = 0; i < 3; i++) {
    counters.push(function () {
      return i
    })
  }

  return counters
}

Now the functions return:

0
1
2

Each loop iteration gets its own binding.

Why IIFE used to appear

Before let was available, developers often used an immediately invoked function expression:

for (var i = 0; i < 3; i++) {
  ;(function (j) {
    counters.push(function () {
      return j
    })
  })(i)
}

The IIFE creates a new function scope and passes the current value as j. In modern code, let is clearer.

Hoisting note

Both var and let declarations are processed before execution, but they behave differently before the declaration line. var can be read as undefined; let is in the temporal dead zone and throws if accessed too early.

console.log(a) // undefined
var a = 1

console.log(b) // ReferenceError
let b = 1

This is another reason let and const catch mistakes earlier.

Subscribe to FreeMac

Weekly picks: free Mac software reviews, trusted source updates, alternatives, and low-friction guides.