FullStack Course LogoFullStack Course
Module: JavaScript
JavaScript·043·10 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 should be able to:

  • declare and initialize bindings with const and let;
  • choose const as the default and use let only when reassignment is required;
  • tell the difference between reassignment and mutation;
  • choose descriptive camel-case names; and
  • explain global, module, function, and block scope at an introductory level.

Prerequisites and Retrieval

Before continuing, retrieve the expressions, statements, literals, and console.log() concepts from 042. Answer these questions first:

  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. If you are rerunning a complete example, refresh first. Otherwise, declarations from an earlier attempt may still exist and cause a name-conflict error.

Terms

  • binding: An association between an identifier and a value in a scope. — Source: ECMA-262: Environments
  • declaration: Syntax that creates a binding, such as const, let, var, or function. — Source: MDN: const
  • initializer: The right-hand value assigned when a declaration is created. — Source: MDN: let
  • assignment: Storing a value in an existing binding with the = operator. — Source: MDN: Assignment operators
  • reassignment: Assigning a new value to a mutable binding, which is allowed for let and var but not for const. — Source: MDN: const
  • identifier: The name of a binding, function, or property that follows JavaScript's naming rules. — Source: MDN: Grammar and types
  • scope: The region of program text where a binding is valid and visible. — Source: MDN: Glossary — Scope
  • block: Zero or more statements grouped with braces {}. A block creates block scope for let and const. — Source: MDN: Block statement
  • shadowing: An inner-scope binding hiding an outer binding with the same name. — Source: MDN: Glossary — Scope
  • mutation: Changing an existing object or array value instead of replacing the 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

Raw values are useful for a quick expression, as in console.log(120 * 3);. In a real program, a meaningful name makes that value easier to understand and reuse: const ticketPrice = 120;. A useful mental model is to treat the name as a label for the current value. The label carries business meaning, and it means you do not have to repeat the raw value throughout the program.

Use const when the binding will not receive another assignment:

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

A const declaration needs an initializer immediately, so const total; is invalid. Reassigning a constant also fails: courseDays = 21;. The word "constant" describes the binding, not necessarily the contents of an object or array. That distinction becomes important in 052 (arrays) and 056 (objects).

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

js
let completedLessons = 0;
completedLessons = 1;

The progress value changes here, so let communicates that intent. Do not choose let simply because the subject might change in the real world. A value changes in this program only when the code reassigns it.

The = operator means assignment, not mathematical equality. In const price = 50;, JavaScript evaluates the right-hand side and uses the result to initialize price. In let stock = 10; stock = 9;, the second statement replaces the value in an existing binding. Strict equality uses === and is covered in depth in 045.

JavaScript is case-sensitive: userName and username are different identifiers. Prefer descriptive camel case such as productPrice, isEnrolled, and remainingSeats. An identifier cannot start with a digit, contain spaces or hyphens, or be a reserved word. Avoid unexplained abbreviations, and make sure the name describes what it holds; userName should not secretly contain a price.

Scope as visibility

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

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

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

Top-level declarations in a module have module scope. Function declarations create function scope, which you will study in 050. As a practical rule, keep each binding in the smallest scope that is useful. That reduces accidental interaction between parts of the program and makes name resolution easier to follow.

You will still encounter var in older code. It is function-scoped rather than block-scoped and has confusing historical behavior when used before assignment. Recognize it when reading existing code, 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 rather than deciding on const or let by habit. The name, course, and total receive no later assignments, so they use const. completedLessons changes from 6 to 7, so it uses let. remainingLessons is calculated after the progress update and is never reassigned, so it is also a const. const does not mean that a value must be known before the program starts. It means that the binding receives no later assignment.

For completedLessons = completedLessons + 1, JavaScript reads the old value on the right, adds one, and stores the result back in the writable binding. The shorthand completedLessons += 1 does the same thing. The expanded form is shown here because it makes the first data-flow trace easier to read.

Intermediate Example: Product Data and Block Scope

The next example combines a simple stock update with an inner block:

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 the outer scope. It can also reassign the outer stockCount. The reverse does not work: code in the outer scope cannot read the inner saleValue. Scope determines where a name can be resolved; it does not copy values into other scopes or undo a reassignment.

Declaring another stockCount inside the block would shadow the outer name. That is valid JavaScript, but it makes the trace harder to follow without adding anything useful:

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

Shadowing has legitimate uses, but it is often clearer to choose a more precise name instead.

Optional Advanced Extension: Constant Binding, Mutable Value

An array is an object value. const prevents the binding from being replaced, but it does not freeze the array's contents:

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

Expected output is ['HTML', 'CSS', 'JavaScript'] (the console may use a different quote style). The skills label still refers to the same array after push(), while the contents of that array have changed. That is mutation, not reassignment, and it is not a reason to use let. Use let only when the binding itself must refer 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 determines where JavaScript can resolve that binding.

js
const storeName = "Amsavalli";

function printReceipt() {
  const orderId = 42;

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

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

When resolving a name, JavaScript searches outward through lexical scopes: block, then function, then outer scope, and finally global scope.

var, let, and const are not interchangeable

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

The var declaration is hoisted and its binding is initialized to undefined.

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

The b binding exists in the scope before the declaration executes, but it remains in the temporal dead zone until initialization. Trying to read it during that interval throws a ReferenceError.

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 receives a new let binding, so each function closes over the expected iteration value. Repeating the same pattern 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 intentionally studying or maintaining legacy behavior.

Mistakes and Debugging

  • SyntaxError: Missing initializer in const declaration: provide an initial value, or use let only when delayed initialization is genuinely necessary.
  • TypeError: Assignment to constant variable: the code attempted to reassign a const. Decide whether reassignment is intended and, if it is, declare the binding with let from the beginning.
  • 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 when rerunning repeated lessons.
  • Unexpected undefined: let result; declares a binding without initializing it, so its value is undefined. Initialize close to the declaration whenever possible.
  • Accidental global: declare every binding. 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 using the same identifier.

Once object shorthand is familiar, console.log({ productName, stockCount }); can be useful. For now, use a clear label and value. When something fails, read the first failing line and inspect its declaration and scope 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 so errors and diffs stay readable.
  • 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 with const. Declare completed assignments with let, starting at 2, and 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 the block. 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 is the only binding that 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 the braces. message belongs to the outer scope, and that scope includes the inner block, so the inner code can read it.

Recap and Exit Questions

Variables give values meaningful names that can be reused. const protects a binding from reassignment, while let communicates that the binding is expected to be reassigned. Both are block-scoped. Scope determines where a name is visible; mutation changes the contents of an object without necessarily changing the binding that refers to it.

  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 code outside 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; and 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 describe this as JavaScript physically moving every line to the top. Instead, trace binding creation, initialization, and execution order. In particular, explain why a function expression may have a binding before that binding contains a callable function value.

Reader page: /javascript/lesson/043/variables-scope-hoisting-and-the-temporal-dead-zone