JAVASCRIPT • SYNTAX AND CORE TYPES

var vs. let/const — Explain var vs let/const at a high level (scope/hoisting) (conceptual)

Understanding how JavaScript's three variable keywords differ in scope, hoisting, and safety.

Historical Context & Motivation

When JavaScript was first created in 1995, the language had only one way to declare a variable: the var keyword. For nearly two decades, every JavaScript developer used var for everything — numbers, strings, objects, and more. While this simplicity seemed convenient, it actually introduced subtle bugs that frustrated programmers on a daily basis.

The core issue was that var behaved in ways that were surprising even to experienced developers. Variables declared with var could be used before their declaration line, they ignored block boundaries like if and for blocks, and they could be accidentally redeclared without any error. As web applications grew larger and more complex, these quirks became serious sources of bugs.

1995
JavaScript Is Born
Brendan Eich creates JavaScript in just 10 days at Netscape. The var keyword is the only way to declare variables, borrowing loose scoping rules for quick prototyping.
2009
ES5 Standardized
ECMAScript 5 introduces strict mode to catch common mistakes, but var scoping remains unchanged. Developers increasingly voice frustration about accidental variable leaks.
2015
ES6 Introduces let and const
ECMAScript 2015 (ES6) adds let and const — two new keywords with block scoping and stricter rules, solving most of var's design flaws.
2020+
Modern Best Practice
Today, industry style guides from Google, Airbnb, and Mozilla recommend using const by default and let when reassignment is needed. The var keyword is essentially considered legacy.

So what exactly makes let and const better? The answer centers on two key concepts: scope (where a variable is visible) and hoisting (how JavaScript processes variable declarations before running your code). Let's explore both.

Core Principles & Definitions

Before comparing the three keywords, you need to understand three foundational ideas that govern how variables behave in JavaScript. These concepts — scope, hoisting, and reassignment — determine where you can use a variable, when it becomes available, and whether you can change its value after creation.

1

Scope

Scope is the region of your code where a variable is accessible. Think of it like a room in a building — a variable created inside a room can only be used inside that room. JavaScript has two main scope types: function scope (the entire function) and block scope (just the nearest curly braces).
2

Hoisting

Hoisting is JavaScript's behavior of moving declarations to the top of their scope before executing code. With var, the variable is initialized to undefined during hoisting. With let and const, the variable exists but cannot be accessed until its declaration line — a zone called the Temporal Dead Zone (TDZ).
3

Reassignment vs. Redeclaration

Reassignment means changing a variable's value after it's been created. Redeclaration means declaring the same variable name again in the same scope. var allows both freely. let allows reassignment but blocks redeclaration. const blocks both.
4

Block vs. Function Scope

A block is any code wrapped in curly braces { } — this includes if statements, for loops, and while loops. Variables declared with var ignore block boundaries and belong to the entire enclosing function, while let and const respect them.
✦ KEY TAKEAWAY
Think of var like writing your name on a whiteboard that the whole classroom can see — anyone in the function can access it. let and const are like writing your name on a sticky note that stays on one desk — only the code inside that specific block can read it. This containment prevents your variable from accidentally interfering with code outside its intended area.

Visualizing Scope

The diagram below illustrates the fundamental difference between function scope and block scope. Notice how a var variable declared inside an if block leaks out into the entire function, while let stays contained within the block where it was declared.

Left: var age (yellow) is accessible outside the if block because var is function-scoped. Right: let age (cyan) stays inside the if block, causing a ReferenceError when accessed outside.

The left panel shows the dangerous behavior of var: even though age was declared inside the if block, it "leaks" into the surrounding function and logs 16 successfully. The right panel shows the safer behavior of let: attempting to access age outside the block throws an error, alerting you to the mistake immediately rather than letting a hidden bug slip through.

How Hoisting Works Under the Hood

When JavaScript runs your code, it doesn't simply read from top to bottom. Instead, the engine performs a two-phase process. In the first phase — the creation phase — the engine scans all declarations and sets up memory space for them. In the second phase — the execution phase — it runs the code line by line. This two-step process is what creates the behavior we call hoisting.

var Hoisting: Initialized to undefined

During the creation phase, a var declaration is hoisted to the top of its function and initialized to undefined. This means you can reference the variable before its declaration line without getting an error — you just get undefined instead of the value you expected. This silent failure is one of the most common sources of bugs in JavaScript.

let/const Hoisting: The Temporal Dead Zone

Technically, let and const are also hoisted — the engine knows they exist during the creation phase. However, they are not initialized. The region between the top of the block and the actual declaration line is called the Temporal Dead Zone (TDZ). Any attempt to access the variable in this zone throws a ReferenceError, which immediately tells you something is wrong.

Left: var is hoisted and initialized to undefined, silently hiding the bug. Right: let enters the Temporal Dead Zone and throws an error, catching the mistake immediately.
💡 Why Does This Matter?
Imagine you accidentally use a variable 50 lines before you declare it. With var, your program runs without complaint but uses undefined, potentially causing weird behavior that's extremely hard to track down. With let or const, the program crashes immediately with a clear error message pointing you to the exact line — saving you hours of debugging.

Detailed Comparison: var, let, and const

Now that you understand scope and hoisting conceptually, let's put all three keywords side by side and compare their behavior across every important dimension. This table is your quick-reference guide whenever you're unsure which keyword to use.

Complete feature comparison of JavaScript variable declaration keywords
Featurevarletconst
ScopeFunction-scopedBlock-scopedBlock-scoped
HoistingHoisted, initialized to undefinedHoisted, but in TDZ (not initialized)Hoisted, but in TDZ (not initialized)
RedeclarationAllowed (no error)Not allowed (SyntaxError)Not allowed (SyntaxError)
ReassignmentAllowedAllowedNot allowed (TypeError)
Must initialize?NoNoYes — must assign a value
Global object property?Yes (creates window.x)NoNo

Understanding const with Objects

One common misconception is that const makes everything immutable (unchangeable). In reality, const only prevents you from reassigning the variable itself. If the value is an object or array, you can still modify its contents. For example, const arr = [1, 2]; means you cannot do arr = [3, 4]; (reassignment), but you can do arr.push(3); (mutation) because the variable still points to the same array in memory.

Restrictiveness Spectrum
var
let
const
Function scope, hoists to undefined, allows redeclaration
Block scope, TDZ, allows reassignment
Block scope, TDZ, no reassignment
Least restrictiveMost restrictive

Worked Example: Debugging a Loop

One of the most famous bugs caused by var happens inside loops. Let's walk through a scenario where using var causes unexpected behavior, and then fix it using let.

The Classic Loop Bug
1
Step 1 — The Buggy CodeConsider this code that tries to create three functions, each printing a different number: for (var i = 0; i < 3; i++) { setTimeout(function() { console.log(i); }, 100); }. You might expect this to print 0, 1, 2 — but it actually prints 3, 3, 3. Let's understand why.
2
Step 2 — Identify the Scope IssueBecause var i is function-scoped, there is only one single variable i shared by all three iterations of the loop. By the time the setTimeout callbacks run (100ms later), the loop has already finished and i has been incremented to 3.
All three callbacks read the same i, which is now 3
3
Step 3 — Fix with letSimply change var to let: for (let i = 0; i < 3; i++) { setTimeout(function() { console.log(i); }, 100); }. Because let is block-scoped, each iteration of the loop creates a fresh copy of i scoped to that specific iteration.
Output: 0, 1, 2 — each callback has its own i
4
Step 4 — Verify the ConceptWith let, you can imagine the loop unrolling into three separate blocks, each with its own variable: block 1 has i = 0, block 2 has i = 1, block 3 has i = 2. The callbacks capture their own block's i, preserving the correct value.
Rule: Always use let (or const) in loops to avoid shared-variable bugs.

Strengths, Limitations & When to Use Each

While modern best practice heavily favors const and let, it's important to understand the trade-offs and know when each keyword is appropriate. Understanding var is still necessary because you'll encounter it in older codebases, tutorials, and Stack Overflow answers.

Practical trade-offs for each variable declaration keyword
KeywordStrengthsLimitationsWhen to Use
varWorks in all JavaScript versions; useful in rare advanced closuresFunction-scoped, hoists to undefined, allows redeclaration — all lead to hidden bugsLegacy code maintenance only; avoid in new code
letBlock-scoped, TDZ catches errors early, prevents accidental redeclarationRequires ES6+ environments; some developers overuse it when const would be betterWhen you know the variable will be reassigned (counters, accumulators, flags)
constAll benefits of let plus immutable binding; signals intent clearly to other developersMust be initialized at declaration; doesn't prevent mutation of objects/arraysDefault choice for all variables; use unless reassignment is needed
🎯 THE MODERN RULE OF THUMB
Think of choosing a variable keyword like choosing a lock for a door. const is a deadbolt — secure by default, and the right choice for most doors. let is a regular lock — still safe, but you use it when you need to open the door sometimes (reassign the value). var is leaving the door unlocked — it might work, but you're asking for trouble. Start with const, downgrade to let only when you must, and avoid var entirely in new code.

Connection to Advanced Topics

Understanding scope and hoisting is foundational, but these concepts connect directly to more advanced JavaScript topics you'll encounter as you continue learning. The way variables interact with scope is at the heart of some of JavaScript's most powerful — and sometimes confusing — features.

How scope and hoisting connect to advanced JavaScript concepts
Concept You LearnedAdvanced Topic It Leads ToHow They Connect
Block scope (let/const)ClosuresClosures capture variables from their enclosing scope; block scope ensures each closure captures the correct value.
Temporal Dead ZoneExecution ContextThe TDZ is part of how JavaScript's execution context and lexical environment work under the hood.
const immutabilityImmutable Data & Object.freeze()const prevents rebinding; Object.freeze() prevents mutation. Together they create truly immutable data.
Function scope (var)IIFE PatternBefore let/const, developers used Immediately Invoked Function Expressions to create private scope — a workaround for var's limitations.

As you progress, you'll see that mastering closures — functions that "remember" the variables from the scope where they were created — becomes much easier once you fully understand block scoping. The loop example from Section 6 was actually a closure problem at its core. The IIFE (Immediately Invoked Function Expression) pattern, which you may see in older code, was essentially a manual way to create block-like scoping before let existed. Understanding why these patterns exist gives you deeper insight into JavaScript's evolution.

Practice Problems

Test your understanding of scope, hoisting, and the differences between var, let, and const with these five problems. Try to answer each one before reading the solution.

PROBLEM 1 — CONCEPTUAL
Explain in your own words why let is said to be "block-scoped" while var is "function-scoped." Give a one-sentence example scenario where this difference matters.
PROBLEM 2 — BASIC CALCULATION
What does the following code output and why? console.log(x); var x = 5; console.log(x);
PROBLEM 3 — INTERMEDIATE
Consider: let score = 100; if (true) { let score = 50; console.log(score); } console.log(score); What are the two outputs? Now replace both let keywords with var. What changes and why?
PROBLEM 4 — APPLIED
You're building a student grade tracker. You need to store a student's name (which won't change), a running total of their scores (which will change as grades are entered), and a temporary variable inside a loop that processes each grade. Which keyword (var, let, or const) would you use for each, and why?
PROBLEM 5 — CRITICAL THINKING
A classmate argues: "Since const prevents changes, I should use it for an array of student names so nobody can modify the list." They write const students = ["Ana", "Ben"]; students.push("Cal"); and are surprised it works. Explain why const didn't prevent the push(), and describe what const actually prevents.

Lesson Summary

JavaScript provides three keywords for declaring variables: var, let, and const. The original var keyword is function-scoped — it ignores block boundaries and is hoisted with an initial value of undefined, which can silently introduce bugs. The ES6 keywords let and const are block-scoped and enter a Temporal Dead Zone (TDZ) where accessing them before their declaration throws an error, making bugs easier to catch.

Modern best practice follows a simple hierarchy: use const by default for any variable that won't be reassigned, switch to let when reassignment is necessary (like loop counters or accumulators), and avoid var in new code. Remember that const prevents reassignment of the variable binding, not mutation of objects or arrays. These scoping and hoisting concepts are foundational to understanding more advanced topics like closures and execution contexts.

Varsity Tutors • JavaScript • var vs. let/const — Scope and Hoisting