043: 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
constandlet; - choose
constas the default and useletonly 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:
- What value does
6 * 7produce? - Which browser tool displays logged values and errors?
- 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, orfunction. — 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
letandvarbut not forconst. — 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 forletandconst. — 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:
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:
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:
{
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:
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:
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:
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:
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:
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:
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.
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
console.log(a); // undefined
var a = 10;
The var declaration is hoisted and its binding is initialized to undefined.
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
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 useletonly when delayed initialization is genuinely necessary.TypeError: Assignment to constant variable: the code attempted to reassign aconst. Decide whether reassignment is intended and, if it is, declare the binding withletfrom 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 isundefined. 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
constby default; change toletonly when a later assignment is required. - Never use
varin 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) andis/hasprefixes 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 asMAX_ATTEMPTSfor 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
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.
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:
Product: Pen set
Revenue: 240
Remaining stock: 21
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.
- Why is
constthe default choice? - When is
letappropriate? - What are declaration, initialization, and reassignment?
- Why can code inside a block read an outer binding while code outside cannot read an inner binding?
- Does
constmake an array's contents unchangeable?
Official References
- MDN: Grammar and types, declarations
- MDN:
const - MDN:
let - MDN: block statement
- ECMA-262: let and const declarations
- ECMA-262: identifiers
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.
