# How var, let, and const Differ in Hoisting and Scope: A Complete Guide from You-Dont-Know-JS

> Understand the crucial differences between var let and const in Javascript hoisting and scope. Master JS fundamentals with this complete guide to avoid common pitfalls and write cleaner code.

- Repository: [Kyle Simpson/You-Dont-Know-JS](https://github.com/getify/You-Dont-Know-JS)
- Tags: deep-dive
- Published: 2026-02-24

---

**`var` is function-scoped, hoisted with immediate initialization to `undefined`, and allows re-declaration; `let` and `const` are block-scoped, hoisted but left uninitialized in the Temporal Dead Zone (TDZ) until their declaration line executes, and forbid re-declaration in the same scope.**

The differences between `var`, `let`, and `const` in JavaScript extend far beyond simple syntax preferences. According to the authoritative source code analysis in the **You-Dont-Know-JS** repository by Kyle Simpson, these declaration keywords exhibit fundamentally distinct behaviors regarding **hoisting** and **scope** that directly impact how the JavaScript engine registers and initializes identifiers. Understanding these mechanics is essential for avoiding the subtle bugs caused by the Temporal Dead Zone and scope leakage.

## Function Scope vs. Block Scope

The most fundamental distinction begins with where each declaration creates its binding.

**`var`** declarations are **function-scoped**. When declared inside a function, the identifier is accessible throughout the entire function body regardless of where the declaration appears. If declared outside any function, `var` creates a global property on the global object.

**`let`** and **`const`** declarations are **block-scoped**. They define identifiers that are only accessible within the nearest enclosing `{ … }` block, whether that block belongs to an `if` statement, a `for` loop, or a standalone block. This prevents the "leakage" of loop counters and temporary variables into surrounding scopes.

As documented in [`scope-closures/ch2.md`](https://github.com/getify/You-Dont-Know-JS/blob/main/scope-closures/ch2.md) and [`scope-closures/ch6.md`](https://github.com/getify/You-Dont-Know-JS/blob/main/scope-closures/ch6.md), this block-scoping behavior eliminates many of the accidental variable collisions that plagued pre-ES6 JavaScript codebases.

## Hoisting Mechanics: How the Engine Registers Identifiers

Despite common misconceptions, **all three declarations are hoisted**—the JavaScript engine registers the identifier name at the top of its respective scope during the compilation phase. However, the critical difference lies in **when and how the identifier is initialized**.

### var Hoisting and Auto-Initialization

When the engine encounters a `var` declaration, it hoists the identifier to the top of its enclosing function (or global) scope and **immediately initializes it to `undefined`**. This means the variable name exists and holds a value from the very beginning of the scope's execution.

```javascript
console.log(greeting); // → undefined (name exists, value not yet set)
var greeting = 'Hello';
console.log(greeting); // → 'Hello'

```

As explained in [`scope-closures/ch5.md`](https://github.com/getify/You-Dont-Know-JS/blob/main/scope-closures/ch5.md), this behavior creates the well-known "variable hoisting" effect where statements appearing before the `var` declaration can still reference the variable, albeit with an `undefined` value until the assignment executes.

### let and const Hoisting with the Temporal Dead Zone

`let` and `const` declarations are also hoisted to the top of their enclosing block, but the engine **does not initialize them** immediately. Instead, the identifier remains in an uninitialized state until the execution reaches the actual declaration line.

This period between the start of the block and the declaration line is known as the **Temporal Dead Zone (TDZ)**. During the TDZ, the identifier exists but cannot be accessed—any attempt to reference it throws a `ReferenceError`.

```javascript
// console.log(message); // ❌ ReferenceError: Cannot access 'message' before initialization
let message = 'Hi';
console.log(message);   // → 'Hi'

```

According to [`scope-closures/ch5.md`](https://github.com/getify/You-Dont-Know-JS/blob/main/scope-closures/ch5.md), this design prevents the accidental use of uninitialized values while still allowing the engine to know the identifier exists for lexical analysis and scope resolution.

## The Temporal Dead Zone (TDZ) Explained

The **Temporal Dead Zone** represents one of the most critical behavioral differences between `var` and the ES6 declarators. While `var` allows access (returning `undefined`) before its declaration, `let` and `const` enforce a strict "declaration-before-use" policy through the TDZ.

For `const`, the TDZ carries an additional constraint: the declaration **must include an initializer**. You cannot declare a `const` without assigning a value, as the syntax itself requires initialization.

```javascript
const PI = 3.14159;
// const PI;               // ❌ SyntaxError: Missing initializer
// PI = 3;                // ❌ TypeError: Assignment to constant variable.
console.log(PI);         // → 3.14159

```

As detailed in [`scope-closures/ch5.md`](https://github.com/getify/You-Dont-Know-JS/blob/main/scope-closures/ch5.md), the TDZ ends precisely when the JavaScript engine evaluates the initializer expression during the declaration line's execution.

## Re-declaration and Re-assignment Rules

Beyond hoisting and scoping, the three declarators enforce different rules regarding duplicate declarations and value assignment:

- **`var`**: Allows re-declaration within the same scope (subsequent `var` declarations of the same name are effectively no-ops) and permits re-assignment at any time.
- **`let`**: Forbids re-declaration within the same block scope (throws `SyntaxError`) but allows re-assignment after initialization.
- **`const`**: Forbids re-declaration and **forbids re-assignment** entirely (throws `TypeError` on assignment attempts). Note that `const` prevents rebinding of the identifier but does not make objects immutable.

## Practical Code Examples from You-Dont-Know-JS

The following examples demonstrate the practical implications of these differences as documented in the repository's `scope-closures` chapters.

### Block Scope Eliminates Variable Leakage

Unlike `var`, which would leak the loop counter into the surrounding function, `let` confines `i` and `square` to the `for` block:

```javascript
for (let i = 0; i < 3; i++) {
  const square = i * i;
  console.log(i, square);
}
// console.log(i);   // ❌ ReferenceError: i is not defined
// console.log(square); // ❌ ReferenceError: square is not defined

```

As shown in [`scope-closures/ch6.md`](https://github.com/getify/You-Dont-Know-JS/blob/main/scope-closures/ch6.md), this block-scoping behavior prevents accidental variable collisions and makes closures in loops behave predictably.

## Summary

- **`var`** declarations are **function-scoped**, **hoisted with immediate initialization to `undefined`**, and permit re-declaration within the same scope.
- **`let`** and **`const`** declarations are **block-scoped**, **hoisted but not initialized** (creating a Temporal Dead Zone until the declaration line), and forbid re-declaration within the same block.
- The **Temporal Dead Zone** prevents access to `let` and `const` variables before their declaration line executes, throwing `ReferenceError` on premature access.
- **`const`** requires initialization at declaration and prevents re-assignment, while **`let`** allows both initialization and later re-assignment.

## Frequently Asked Questions

### Are let and const hoisted like var?

Yes, `let` and `const` are technically hoisted—the JavaScript engine registers the identifier name at the top of the enclosing block during the compilation phase. However, unlike `var`, they are not initialized immediately. They remain in the **Temporal Dead Zone** until the execution reaches the declaration line, making them behave as if they are not hoisted in practice.

### What is the Temporal Dead Zone in JavaScript?

The **Temporal Dead Zone (TDZ)** is the period between the start of a block and the execution of a `let` or `const` declaration line. During this zone, the identifier exists but cannot be accessed—any attempt to read or write the variable throws a `ReferenceError`. The TDZ ends precisely when the engine evaluates the initializer expression during the declaration's execution.

### Can I redeclare a variable with let in the same scope?

No, attempting to declare a `let` variable with the same name in the same block scope throws a `SyntaxError`. This differs from `var`, which allows re-declaration within the same function scope (treating subsequent declarations as no-ops). The same prohibition applies to `const`—you cannot redeclare a `const` binding in the same scope.

### Why does const prevent reassignment but not mutation?

`const` creates a **read-only binding** between the identifier and the value stored at the time of declaration. This prevents re-assignment of the identifier itself (the binding), but if the value is an object or array, the contents of that object can still be modified. To prevent mutation, you would need to use `Object.freeze()` or similar techniques in addition to `const`.