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.
var keyword is the only way to declare variables, borrowing loose scoping rules for quick prototyping.var scoping remains unchanged. Developers increasingly voice frustration about accidental variable leaks.let and const — two new keywords with block scoping and stricter rules, solving most of var's design flaws.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.
Scope
Hoisting
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).Reassignment vs. Redeclaration
var allows both freely. let allows reassignment but blocks redeclaration. const blocks both.Block vs. Function Scope
{ } — 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.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.
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.
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.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.
| Feature | var | let | const |
|---|---|---|---|
| Scope | Function-scoped | Block-scoped | Block-scoped |
| Hoisting | Hoisted, initialized to undefined | Hoisted, but in TDZ (not initialized) | Hoisted, but in TDZ (not initialized) |
| Redeclaration | Allowed (no error) | Not allowed (SyntaxError) | Not allowed (SyntaxError) |
| Reassignment | Allowed | Allowed | Not allowed (TypeError) |
| Must initialize? | No | No | Yes — must assign a value |
| Global object property? | Yes (creates window.x) | No | No |
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.
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.
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.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.i, which is now 3var 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.ilet, 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.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.
| Keyword | Strengths | Limitations | When to Use |
|---|---|---|---|
| var | Works in all JavaScript versions; useful in rare advanced closures | Function-scoped, hoists to undefined, allows redeclaration — all lead to hidden bugs | Legacy code maintenance only; avoid in new code |
| let | Block-scoped, TDZ catches errors early, prevents accidental redeclaration | Requires ES6+ environments; some developers overuse it when const would be better | When you know the variable will be reassigned (counters, accumulators, flags) |
| const | All benefits of let plus immutable binding; signals intent clearly to other developers | Must be initialized at declaration; doesn't prevent mutation of objects/arrays | Default choice for all variables; use unless reassignment is needed |
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.
| Concept You Learned | Advanced Topic It Leads To | How They Connect |
|---|---|---|
| Block scope (let/const) | Closures | Closures capture variables from their enclosing scope; block scope ensures each closure captures the correct value. |
| Temporal Dead Zone | Execution Context | The TDZ is part of how JavaScript's execution context and lexical environment work under the hood. |
| const immutability | Immutable Data & Object.freeze() | const prevents rebinding; Object.freeze() prevents mutation. Together they create truly immutable data. |
| Function scope (var) | IIFE Pattern | Before 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.
let is said to be "block-scoped" while var is "function-scoped." Give a one-sentence example scenario where this difference matters.console.log(x); var x = 5; console.log(x);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?var, let, or const) would you use for each, and why?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.