Module: JavaScript
JavaScript·043·9 MIN READ

043: Variables, Scope, Hoisting, and the Temporal Dead Zone

TOPICS COVERED: Variables, Scope, Hoisting, and the Temporal Dead Zone

Outcomes

By the end of this lesson, you can:

  • declare and initialize bindings with const and let;
  • choose const by default and let only for reassignment;
  • distinguish reassignment from mutation;
  • use descriptive camel-case names; and
  • explain global, module, function, and block scope at an introductory level.

Prerequisites and Retrieval

From 042, retrieve expressions, statements, literals, and console.log(). Answer before continuing:

  1. What value does 6 * 7 produce?
  2. Which browser tool displays logged values and errors?
  3. Why might parentheses improve 2 * 10 + 5?

Run today's examples in a clean browser console or a module script. Refresh before rerunning a full example so declarations from an earlier attempt do not conflict.

Terms

  • binding: An association between an identifier and a value in a scope. — Source: ECMA-262: Environments
  • declaration: Syntax creating a binding: const, let, var, function. — Source: MDN: const
  • initializer: The right-hand value assigned when a declaration is created. — Source: MDN: let
  • assignment: Storing a value into an existing binding via the = operator. — Source: MDN: Assignment operators
  • reassignment: Assigning a new value to a mutable binding (allowed for let/var, not const). — Source: MDN: const
  • identifier: The name of a binding, function, or property following naming rules. — Source: MDN: Grammar and types
  • scope: The region of program text in which a binding is valid and visible. — Source: MDN: Glossary — Scope
  • block: Zero or more statements grouped with braces {} creating a block scope for let/const. — Source: MDN: Block statement
  • shadowing: An inner-scope binding hiding a same-named outer binding. — Source: MDN: Glossary — Scope
  • mutation: Changing an existing object/array value rather than replacing a binding’s reference. — Source: MDN: Glossary — Mutable
  • Variable (official): "A variable is a named container for a value." — Source: MDN Glossary: Variable
  • Hoisting: "Variable and function declarations are conceptually moved to the top of their scope before execution; let/const remain in temporal dead zone until initialization." — Source: MDN: let — Temporal Dead Zone
  • Temporal Dead Zone (TDZ): "The period between entering scope and the actual declaration where accessing let/const throws ReferenceError." — Source: MDN: let — TDZ

Beginner Explanation and Mental Model

A literal repeats raw information: console.log(120 * 3);. A variable gives a value a meaningful name: const ticketPrice = 120;. Think of the name as a stable label pointing to the current value. Names let code communicate business meaning and reuse a value without typing it repeatedly.

Use const when the binding will not be assigned another value:

js
const userName = "Maya";
const courseDays = 20;

A const declaration must have an initializer. This is invalid: const total;. Reassigning a constant also fails: courseDays = 21;. The word "constant" applies to the binding, not necessarily the inside of an object or array. That distinction becomes important in 052 (arrays) and 056 (objects).

Use let when the program genuinely needs to replace a value:

js
let completedLessons = 0;
completedLessons = 1;

Here your progress changes, so let makes that intent visible. Do not use let merely because something might change in the real world. A program value changes only if this code reassigns it.

= means assignment, not mathematical equality. The declaration const price = 50; evaluates the right side and initializes price. Later, let stock = 10; stock = 9; replaces the binding's value. Strict equality is written === and is covered in depth in 045.

JavaScript is case-sensitive. userName and username are different. Prefer descriptive camel case: productPrice, isEnrolled, and remainingSeats. Names cannot start with a digit, contain spaces or hyphens, or be reserved words. Avoid unexplained abbreviations and misleading names such as userName holding a price.

Scope as visibility

const and let are block-scoped. A binding declared inside braces is available from its declaration to the end of that block, not outside it:

js
{
  const message = "Inside";
  console.log(message);
}

// console.log(message); // ReferenceError

Top-level module declarations have module scope. Function declarations create function scope, studied in 050. Keep bindings in the smallest useful scope: it limits accidental interaction and makes code easier to understand.

You may encounter var in old code. It is function-scoped rather than block-scoped and has confusing historical behavior around use before assignment. Recognize it, but do not use it in new course code.

Worked Example: User Progress

Run this complete program in a clean console:

js
const userName = "Asha";
const courseName = "JavaScript Foundations";
const totalLessons = 20;
let completedLessons = 6;

console.log("", userName);
console.log("Course:", courseName);
console.log("Completed:", completedLessons);

completedLessons = completedLessons + 1;

const remainingLessons = totalLessons - completedLessons;
console.log("After today's lesson:", completedLessons);
console.log("Remaining:", remainingLessons);

Expected output:

text
Asha
Course: JavaScript Foundations
Completed: 6
After today's lesson: 7
Remaining: 13

Trace the data flow. Your name, course, and total do not receive new assignments, so each uses const. completedLessons changes from 6 to 7, so it uses let. remainingLessons is calculated only after progress changes and is never reassigned, so it is still a const. const does not mean "known before the program starts"; it means "this binding receives no later assignment."

The expression on the right side of completedLessons = completedLessons + 1 reads the old value, adds one, then stores the result back into the writable binding. The similar shorthand completedLessons += 1 is valid, but the expanded form is clearer during the first trace.

Intermediate Example: Product Data and Block Scope

This example models a simple stock update and demonstrates inner scope:

js
const productName = "Notebook";
const unitPrice = 80;
let stockCount = 12;
const quantitySold = 3;

{
  const saleValue = unitPrice * quantitySold;
  stockCount = stockCount - quantitySold;

  console.log("Sale value:", saleValue);
  console.log("Stock inside block:", stockCount);
}

console.log("Product:", productName);
console.log("Stock after sale:", stockCount);
// console.log(saleValue); // ReferenceError: saleValue is not defined

Expected output:

text
Sale value: 240
Stock inside block: 9
Product: Notebook
Stock after sale: 9

The inner block can read unitPrice and quantitySold from its outer scope and can reassign the outer stockCount. However, the outer scope cannot read the inner saleValue. Scope controls where a name can be resolved; it does not automatically copy a value or undo a reassignment.

Avoid declaring another stockCount inside the block. That would shadow the outer name, making the trace needlessly confusing:

js
const status = "outside";
{
  const status = "inside";
  console.log(status); // inside
}
console.log(status); // outside

Shadowing is valid but often avoidable with a more precise name.

Optional Advanced Extension: Constant Binding, Mutable Value

An array is an object value. const prevents replacement of the binding, but the array can still be mutated:

js
const skills = ["HTML", "CSS"];
skills.push("JavaScript");
console.log(skills);
// skills = ["Git"]; // TypeError: assignment to constant variable

Expected output is ['HTML', 'CSS', 'JavaScript'] (quote style varies by console). The label skills still points to the same array after push(), while that array's contents changed. This is not a reason to use let; use let only if the binding itself must point to a different value. Array mutation is taught properly in 052–053.

Deep Dive: Bindings, Scope Chains, Hoisting, and TDZ

A variable declaration creates a binding between a name and a value. Scope controls where that binding can be resolved.

js
const storeName = "Amsavalli";

function printReceipt() {
  const orderId = 42;

  if (orderId > 0) {
    const label = `${storeName} #${orderId}`;
    console.log(label);
  }

  // console.log(label); // ReferenceError
}

Resolution walks outward through lexical scopes: block → function → outer scope → global scope.

var, let, and const are not interchangeable

js
console.log(a); // undefined
var a = 10;

The declaration is hoisted and initialized to undefined.

js
console.log(b); // ReferenceError
let b = 10;

b exists in the scope before the declaration executes, but it remains inside the temporal dead zone until initialization.

A loop closure example

js
const handlers = [];

for (let i = 0; i < 3; i += 1) {
  handlers.push(() => i);
}

console.log(handlers[0]()); // 0
console.log(handlers[1]()); // 1
console.log(handlers[2]()); // 2

Each iteration gets a new let binding. Repeating this with var changes the result because var is function-scoped.

Rule for production code

Prefer const by default. Use let when the binding itself must be reassigned. Avoid var in new code unless you are deliberately studying or maintaining legacy behavior.

Mistakes and Debugging

  • SyntaxError: Missing initializer in const declaration: supply a starting value, or use let only when delayed initialization is genuinely necessary.
  • TypeError: Assignment to constant variable: the code tried to reassign a const. Decide whether reassignment is intended; if so, declare it with let from the start.
  • ReferenceError: name is not defined: check spelling, capitalization, declaration order, and scope.
  • Identifier has already been declared: do not declare the same name twice in one scope. Refresh a console used for repeated lessons.
  • Unexpected undefined: let result; is declared but not initialized, so its value is undefined. Initialize close to declaration where possible.
  • Accidental global: always declare bindings. Assignment to an undeclared name fails in modules and strict mode and can leak global state in older non-strict scripts.
  • Confusing shadowed value: inspect each surrounding block for another declaration with the same identifier.

Use console.log({ productName, stockCount }); later when object shorthand is familiar; for now, log a clear label and value. Read the first failing line before changing declarations at random.

Best Practices

  • Declare with const by default; change to let only when a later assignment is required.
  • Never use var in new course code; recognize it only as historical syntax.
  • Declare one binding per statement for readable errors and diffs.
  • Initialize at declaration when the value is available.
  • Use nouns for data (totalPrice) and is/has prefixes for Boolean meanings (isAvailable).
  • Prefer meaningful names over one-letter names outside short loop counters.
  • Keep scope as small as practical and avoid unnecessary global state.
  • Avoid changing a variable's meaning or type halfway through a program.
  • Do not create uppercase constant names for every const; reserve names such as MAX_ATTEMPTS for true shared configuration when a project style uses that convention.

Tiered Exercises

Core

Declare a user's name, age, and course using const. Declare completed assignments with let, starting at 2, then reassign it to 3. Log all four values.

Practice

Create a product program with a product name, unit price, starting stock, and sold quantity. Calculate revenue and update stock. Choose const or let for each binding and print a readable summary.

Professional Extension

Create an outer const message = "Course". Inside a block, declare const lesson = "Variables", log both names, and then log only message outside. Explain why lesson cannot be read outside. Add a commented line that would demonstrate the error.

Complete Solutions

js
const userName = "Kiran";
const userAge = 19;
const courseName = "Full Stack Development";
let completedAssignments = 2;

completedAssignments = 3;

console.log("Name:", userName);
console.log("Age:", userAge);
console.log("Course:", courseName);
console.log("Completed assignments:", completedAssignments);

Only completedAssignments is rebound, so it alone requires let.

js
const productName = "Pen set";
const unitPrice = 60;
let stock = 25;
const soldQuantity = 4;
const revenue = unitPrice * soldQuantity;

stock = stock - soldQuantity;

console.log("Product:", productName);
console.log("Revenue:", revenue);
console.log("Remaining stock:", stock);

Expected output:

text
Product: Pen set
Revenue: 240
Remaining stock: 21
js
const message = "Course";

{
  const lesson = "Variables";
  console.log(message, lesson);
}

console.log(message);
// console.log(lesson); // ReferenceError: lesson is not defined

lesson belongs to the block bounded by braces. message belongs to the outer scope, which includes the inner block.

Recap and Exit Questions

Variables give values meaningful, reusable names. const protects a binding from reassignment; let communicates planned reassignment. Both are block-scoped. Scope determines name visibility, while mutation changes content inside an object without necessarily changing its binding.

  1. Why is const the default choice?
  2. When is let appropriate?
  3. What are declaration, initialization, and reassignment?
  4. Why can code inside a block read an outer binding while outside code cannot read an inner binding?
  5. Does const make an array's contents unchangeable?

Official References

References checked 2026-08-24.

Hoisting, bindings, and the temporal dead zone

Interview questions often use hoisting imprecisely. Before statements execute, the runtime creates bindings in the relevant environment. Function declarations can be initialized with their function value; var bindings are initialized to undefined; let, const, and class bindings exist but cannot be read before initialization. That inaccessible interval is the temporal dead zone.

Compare a function declaration, a function expression, var, let, const, and a class in a clean file. Do not say that JavaScript physically moves every line to the top. Trace binding creation, initialization, and execution order. Explain why a function expression may have a binding before it has a callable function value.