# Differences Between var, let, and const Declarations in JavaScript

> Understand the key differences between JavaScript var let and const declarations including hoisting scope and mutability Learn how to choose the right declaration for your code

- Repository: [Lydia Hallie/javascript-questions](https://github.com/lydiahallie/javascript-questions)
- Tags: deep-dive
- Published: 2026-03-04

---

**`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](https://github.com/lydiahallie/javascript-questions) repository provides concrete examples demonstrating how these declarations behave under various execution contexts, particularly in [`README.md`](https://github.com/lydiahallie/javascript-questions/blob/main/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](https://github.com/lydiahallie/javascript-questions/blob/master/README.md#L54-L63)):

```javascript
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](https://github.com/lydiahallie/javascript-questions/blob/master/README.md#L88-L95)):

```javascript
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:

```javascript
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();

```

```javascript
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](https://github.com/lydiahallie/javascript-questions/blob/master/README.md#L18-L25)) illustrates this behavior:

```javascript
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:

```javascript
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.