Differences Between var, let, and const Declarations in JavaScript

var, let, and const differ fundamentally in hoisting behavior, scope boundaries, and mutability: var is function-scoped and hoisted with an initial undefined value, while let and const are block-scoped with a temporal dead zone, and const requires initialization and creates an immutable binding.

Understanding the differences between var, let, and const declarations is critical for writing predictable code and avoiding common scope-related bugs. The lydiahallie/javascript-questions repository provides concrete examples demonstrating how these declarations behave under various execution contexts, particularly in README.md where the core interview questions are documented.

Hoisting and the Temporal Dead Zone

One of the most significant distinctions lies in how JavaScript handles hoisting—the mechanism where declarations are moved to the top of their scope during compilation.

When using var, the declaration is hoisted to the top of its containing function or global scope and automatically initialized with undefined. This means accessing the variable before its declaration line returns undefined rather than throwing an error.

In contrast, let and const declarations are hoisted to the top of their block scope but remain uninitialized, creating a temporal dead zone (TDZ). Accessing these variables before the declaration line throws a ReferenceError.

As shown in the first question of the repository (README.md, lines 54-63):

function demo() {
  console.log(name); // undefined (hoisted var)
  console.log(age);  // ReferenceError (let is in TDZ)

  var name = 'Lydia';
  let age = 21;
}
demo();

The var name declaration is hoisted and initialized as undefined, allowing the first console.log to execute. However, let age exists in the TDZ until the initialization line is reached, causing the second console.log to throw an error.

Scope: Function vs Block

The scope model represents another fundamental divergence. var is function-scoped, meaning the variable is visible throughout the entire function in which it is declared, ignoring block boundaries such as if statements or for loops. let and const are block-scoped, restricting visibility to the nearest enclosing pair of curly braces { ... }.

This distinction becomes critical in loop constructs with asynchronous callbacks. According to the second question in the repository (README.md, lines 88-95):

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 1); // prints 3 three times
}

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

The var loop creates a single variable i shared across all iterations, causing all callbacks to reference the final value 3. The let loop creates a fresh, block-scoped binding for each iteration, preserving the expected sequence.

Re-declaration and Initialization Rules

JavaScript enforces different rules regarding variable re-declaration and mandatory initialization.

  • var: Allows re-declaration within the same scope without error (var a; var a;), which can lead to accidental overwrites.
  • let: Throws a SyntaxError if re-declared in the same block scope.
  • const: Throws a SyntaxError if re-declared and must be initialized at the point of declaration (const a = 5;).

The following examples demonstrate how var and let handle scope boundaries differently:

function varExample() {
  var x = 1;
  if (true) {
    var x = 2; // same variable, shadows previous value
    console.log(x); // 2
  }
  console.log(x); // 2 (changed outside the block)
}
varExample();
function letExample() {
  let y = 1;
  if (true) {
    let y = 2; // new block-scoped variable
    console.log(y); // 2
  }
  console.log(y); // 1 (outer variable untouched)
}
letExample();

The var example shows how the declaration leaks out of the if block and overwrites the outer variable, while let properly encapsulates the inner y within the block scope.

Mutability and the const Binding

While let allows reassignment of values, const creates an immutable binding—the variable identifier cannot be reassigned to a new reference. However, if the value is an object, its properties remain mutable.

The third question in the repository (README.md, lines 18-25) illustrates this behavior:

const shape = {
  radius: 10,
  diameter() {
    return this.radius * 2;          // 20
  },
  perimeter: () => 2 * Math.PI * this.radius // NaN
};

console.log(shape.diameter()); // 20
console.log(shape.perimeter()); // NaN

Additionally, attempting to reassign a const binding throws a TypeError, though property mutation is permitted:

function constExample() {
  const z = { a: 1 };
  // z = { a: 2 }; // TypeError: Assignment to constant variable.
  z.a = 2; // Allowed – we mutate the object, not the binding
  console.log(z); // { a: 2 }
}
constExample();

Global Object Behavior

In non-strict mode, var declarations at the top level create a property on the global object (window in browsers, global in Node.js). This can lead to global namespace pollution. let and const do not create properties on the global object, providing better isolation and preventing accidental global variable collisions.

Summary

  • var is function-scoped, hoisted with undefined initialization, allows re-declaration, and creates global object properties.
  • let is block-scoped, hoisted without initialization (temporal dead zone), forbids re-declaration in the same scope, and does not create global properties.
  • const shares let's block-scoping and hoisting rules but requires initialization and creates an immutable binding (though object contents remain mutable).
  • Always prefer let or const over var to avoid scope leakage and hoisting-related bugs.

Frequently Asked Questions

Is let hoisted like var?

Yes, let declarations are hoisted to the top of their block scope, but unlike var, they are not initialized. This creates the temporal dead zone, where accessing the variable before its declaration results in a ReferenceError rather than returning undefined.

Can I reassign a value to a const variable?

No, you cannot reassign the binding—the reference stored in a const variable cannot point to a different memory address or primitive value. However, if the const holds an object or array, you may still mutate its internal properties or elements because only the reference itself is protected.

Why does var leak out of for loops and if blocks?

Because var is function-scoped, not block-scoped. When declared inside a loop or conditional block, JavaScript treats it as if it were declared at the top of the containing function. This causes the variable to persist and be accessible outside the block, often leading to unexpected behavior in closures and asynchronous operations.

Should I always use const instead of let?

Use const by default to signal that a variable should not be reassigned, which helps prevent accidental mutations and communicates intent clearly. Reserve let for values that genuinely need to change (such as loop counters or accumulators), and avoid var entirely in modern JavaScript to eliminate function-scoping issues.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →