Lesson 3 · 12 min read
Var, Let, and Const
All three get hoisted. What happens after that — what value they start with, where they're actually stored in memory, and when you're allowed to touch them — is where they completely part ways.
On this page
- Concept
- Why this matters
- The locked meeting room — an analogy
- var — hoisted and ready
- let and const — hoisted but locked
- const's extra rule
- Block scope vs. function scope
- Under the hood — two memory records
- See it live — window & DevTools
- Real example — traced line by line
- Common misconceptions
- Interview questions
Concept
var, let, and const are all hoisted — memory is reserved for every one of them before your code runs (Lesson 1). That part is identical. What's completely different is what value they start with, whether you're allowed to touch them before their line runs, whether { } blocks contain them, and even which physical part of memory they're stored in. This lesson covers all four differences, with real, checkable examples for each.
Key terms, explained simply
var,let,constare three keywords that declare (create) a variable. A variable is a named place in memory that holds a value.- Declaration vs. assignment. A declaration creates the name (
let a;). An assignment puts a value in it (a = 5;). You can do both in one line:let a = 5;. Giving a variable its first value is also called initialization. - Hoisting is the effect of the memory creation phase: names are created before any code runs, so it looks as if declarations moved to the top of their scope.
- Scope is the part of the code where a name can be seen and used. Block scope means the name only lives inside the nearest pair of
{ }(a block, such as the body of anifor a loop). Function scope means the name lives inside the whole function. - Binding is the link between a variable name and its value. When we say “reassign,” we mean pointing the name at a different value.
- Global object is the one object that the engine creates for the whole program. In a browser it is called
window. Lesson 5 explains it fully. A property is a name and value stored inside an object, such asroom: 101in{ room: 101 }. - Mutate means to change something in place. A thing that cannot be changed is immutable.
- ReferenceError, SyntaxError, TypeError are error types. A ReferenceError means a name cannot be used (it does not exist, or it is locked). A SyntaxError means the code is written in a way the language does not allow. A TypeError means a value is used in a way its type does not allow, such as assigning to a constant.
Why This Matters
- Three declarations can produce three completely different results for the exact same mistake — printing a variable before its line runs — and interviewers use this to check if you understand hoisting properly, not just the keyword.
- Explains why old JS code often had global-variable collisions (two pieces of code using the same global name), and exactly what
let/constwere built to stop. - Sets up the classic
var-inside-a-loop bug withsetTimeout(a function that runs other code after a delay), which you'll trace fully in Lesson 11. - The internal storage split you'll see here also helps you understand the global object and
this, the subject of Lesson 5.
The Locked Meeting Room — An Analogy
var is like an announcement made over the hotel's PA system. The moment it's declared, it's also pinned to the hotel's public noticeboard — where any other script, any other part of the hotel, can read it or overwrite it. Stand in the lobby before the announcement is made, and you just hear silence (undefined) — not an error. And it doesn't matter which room you're in; the announcement reaches the whole building, not just one meeting room.
let/const are a locked meeting room. It's reserved on the day's schedule ahead of time (hoisted), but the door stays locked until the meeting's start time. If you try the handle early, you are turned away and told the room isn't ready yet, not that it's empty. And this room's schedule is kept in a private day-planner, never pinned to the public noticeboard — no other script can read or collide with it. Once the meeting ends (the block closes), everything discussed inside stops existing outside those walls.
const is that same locked room, except the guest assigned to it is locked in for the whole booking — you can rearrange the furniture inside all you like, but that guest can never be swapped out for a different room.
var — Hoisted And Ready
console.log(guestName); // undefined
var guestName = "Aditi";No error. guestName already existed in memory from the memory creation phase, with the value undefined (a special value that means “created, but no value yet”). If you read it early, you get undefined.
let and const — Hoisted But Locked
console.log(guestName); // ReferenceError: Cannot access 'guestName' before initialization
let guestName = "Aditi";let is hoisted too. Many people wrongly believe it is not. The real difference: var gets a placeholder value (undefined) the instant memory is reserved. let/const get locked instead, from the top of the scope until their declaration line actually runs. This locked stretch has a name: the Temporal Dead Zone (TDZ). The TDZ is the time between the start of a scope and the line where a let, const or class is declared. During that time the name exists but cannot be used, and any use throws an error. (“Temporal” means it is about time, not about a place in the file.)
var guestName gets a slot holding undefined. let guestName also gets a slot — it is hoisted too — but the slot is locked: this is the Temporal Dead Zone. The third file declares nothing, so nothing is reserved at all.
1console.log(guestName);2var guestName = "Aditi";
1console.log(guestName);2let guestName = "Aditi";
1console.log(guestName);2// never declared anywhere
| What you do | What happens |
|---|---|
console.log(x) before var x = 5 | undefined — no error |
console.log(x) before let x = 5 (in the TDZ) | ReferenceError: Cannot access 'x' before initialization |
console.log(y), y never declared anywhere | ReferenceError: y is not defined |
Notice these are three genuinely different situations with three different messages. “Cannot access before initialization” (TDZ, it exists but is locked) and “is not defined” (never existed at all) get mixed up often. Knowing them apart is a common interview check. Also note that even typeof x throws while x is in the TDZ.
const's Extra Rule
const roomNumber; // SyntaxError: Missing initializer in const declarationconst must be given a value on the same line where it is declared. This code is a SyntaxError, which JavaScript finds before it runs anything, so none of the file runs.
const guest = { name: "Aditi", room: 101 };
guest.room = 102; // fine — mutating a property
console.log(guest); // { name: "Aditi", room: 102 }
guest = { name: "Rahul" }; // TypeError: Assignment to constant variable.const doesn't freeze the value — it freezes the binding (the variable name itself). You can't point guest at a different object, but you can still reach inside the same object and change what's in it. To stop changes to an object's properties, use Object.freeze(guest) (it freezes the top level only).
Block Scope vs. Function Scope
if (true) {
var a = "visible everywhere";
let b = "visible only inside these { }";
}
console.log(a); // "visible everywhere"
console.log(b); // ReferenceError: b is not definedvar ignores { } blocks — it only respects function boundaries (and the global level). let/const respect { } strictly: the moment the block ends, they're gone.
Under The Hood — Two Memory Records
Here's the part that isn't a comparison. An environment record is the place where the engine stores the names (and values) of one scope. Inside the Global Execution Context's memory, JS keeps two separate records, not one:
- Object Environment Record — an environment record tied to the real global object (
windowin a browser).varand function declarations are stored here, which means they become actual properties on that object. - Declarative Environment Record — a separate, private environment record, not tied to any object at all.
let,const, andclassdeclarations live here instead.
- guestName"Aditi"
- checkIn{ …code }
- holds:
var, function declarations window.guestNameworks- → visible to every script on the page
- roomNumber101
- hotelName"Grand Palace"
- holds:
let,const,class window.roomNumber→undefined- → invisible to other scripts
This two-record split exists only at the global level. Inside a function, all of its names (var, let, const) live in that function's own private record, and none of them become properties of window.
Why this split exists: in early JS, every global var really did become a window property, so two libraries both declaring var user on the same page would silently overwrite each other. let/const (2015) were built specifically to stop this, by keeping their bindings off the global object entirely. This same private record is also exactly what makes the TDZ possible — since these bindings aren't ordinary object properties, the engine can mark one “exists but locked” and throw a real error, instead of just returning undefined like a missing property would.
See It Live — window & DevTools
Check the two records directly
var guestName = "Aditi";
let roomNumber = 101;
const hotelName = "Grand Palace";
console.log(window.guestName); // "Aditi"
console.log(window.roomNumber); // undefined
console.log(window.hotelName); // undefined
console.log(guestName, roomNumber, hotelName); // all three work fine by nameRun this as a real <script> in a browser page — not inside a Node.js file. Node wraps every file in its own function behind the scenes, so even var at the top of a Node file never touches Node's global object either. This experiment only shows the true difference in a browser's actual global scope.
Watch it in the Scope panel
Set a breakpoint
Anywhere after all three declarations have run.
Open the Scope panel
Sources tab → the Scope sidebar, while paused.
Look for two separate buckets
Global — this is
window, and you'll seeguestNamesitting right there as a property. Script — a separate bucket holdingroomNumberandhotelName. That “Script” bucket is the Declarative Environment Record, visible proof it's a different storage area fromwindow.
Real Example — Traced Line By Line
var hotelName = "Grand Palace";
function checkIn(guestName) {
if (guestName) {
let roomNumber = 101;
const confirmed = true;
console.log(guestName + " checked into room " + roomNumber);
}
console.log(typeof roomNumber); // "undefined" — roomNumber never leaked out
}
checkIn("Aditi");Two records inside one context. hotelName (a var) and checkIn (a function) go into the Object Environment Record — they become real properties of window. There is no top-level let or const in this file, so the Declarative Environment Record stays empty.
1var hotelName = "Grand Palace";23function checkIn(guestName) {4if (guestName) {5let roomNumber = 101;6const confirmed = true;7console.log(guestName + " checked into room " + roomNumber);8}9console.log(typeof roomNumber); // "undefined"10}1112checkIn("Aditi");
| hotelName | undefined |
| checkIn | { …code } |
Global EC pushed
hotelName → undefinedreserved, then assigned"Grand Palace".checkInfully hoisted.checkIn("Aditi")called.checkInEC pushedIts own memory:
guestName → "Aditi". Theifblock is entered.Inside the
ifblockroomNumberandconfirmedexist only for these{ }— hoisted into the TDZ at the top of the block, then unlocked the instant their lines run. PrintsAditi checked into room 101.Back outside the block
roomNumberno longer exists here at all —typeof roomNumbersafely prints"undefined"instead of throwing, becausetypeofon a name that was never declared doesn't throw the way reading it directly would. (typeofis an operator that returns the type of a value as a string.)checkInfinishes → poppedIts whole private memory, block-scoped variables included, is discarded completely.
Common Misconceptions
“let isn't hoisted, only var is.” Both are hoisted. The difference is what happens after: var gets undefined immediately, let gets locked in the TDZ until its line runs.
“const means the value can never change.” Only the binding is locked — you can't reassign the variable itself. If it holds an object or array, its contents can still be freely mutated.
“The TDZ is just a lint rule, not something the engine actually enforces.” It's a real runtime check, tied directly to the Declarative Environment Record — accessing a locked binding throws a genuine ReferenceError, not a warning.
Interview Questions
Are let and const hoisted?
Yes — all three are. let/const are hoisted into a locked state (the TDZ) instead of getting undefined immediately.
What's the practical difference between “Cannot access before initialization” and “x is not defined”?
The first means the binding exists but is still in the TDZ (the time before its declaration line runs). The second means the name was never declared anywhere in scope.
Does const make an object immutable?
No — it only locks the binding (the variable name) from being reassigned. Properties inside the object can still be changed freely.
Where do var declarations get stored at the global level, and how can you prove it?
In the Object Environment Record, tied to the real global object — provable by checking window.varName, which returns the value.
Why doesn't window.letName work for a global let?
Because let is stored in the Declarative Environment Record, a separate store never attached to the global object.
Why were let/const introduced, given var already existed?
To stop global variable collisions — since var attaches to the shared global object, two different scripts declaring the same name silently overwrite each other; let/const stay private instead.
Does a let declared inside an if block exist outside of it?
No — let/const are block-scoped; they cease to exist the instant the enclosing { } ends. var would still be accessible outside.
What actually makes the TDZ enforceable at the engine level?
Because let/const bindings live in a private record the engine directly controls, rather than as ordinary object properties, it can flag a binding as “exists but locked” and throw, instead of silently returning undefined the way a missing object property would.
Published by Jainik · Last updated · How these lessons are made
Keep learning
Related series