Chapter 1: Three Keys to the Kingdom
Timothy stared at his browser console in complete confusion. He'd written what seemed like simple JavaScript, but the behavior made no sense.
var x = 5;
if (true) {
var x = 10;
}
console.log(x); // 10 - why?!
"That doesn't make sense," he muttered. "I'm used to languages where blocks protect variables. But JavaScript just... overwrote it?"
He tried again with let:
let y = 5;
if (true) {
let y = 10;
}
console.log(y); // 5 - now it works!
"Wait, what?" Timothy exclaimed, calling out to Margaret who was cataloging new arrivals at the reference desk. "Margaret, come look at this. Why does let work differently than var?"
Margaret set down her book and walked over, reading the code on his screen. She smiled knowingly.
"Ah, you've discovered one of JavaScript's most confusing features—variable scoping. But this isn't a bug, Timothy. It's history. It's a language that evolved while maintaining perfect backward compatibility."
"But they both declare variables! Why are they different?"
"Because," Margaret said, pulling up a chair, "they come from different eras of JavaScript. And to understand the difference, we need to understand how JavaScript thinks about scope."
The Scope Problem
Margaret opened her worn notebook. "When JavaScript was created in 1995, there was only one way to declare variables: var. And var had a quirk. Let me show you."
She wrote:
function greet(name) {
if (name) {
var message = "Hello, " + name;
}
console.log(message); // Prints the message!
}
greet("Alice"); // "Hello, Alice"
Timothy frowned. "The variable created inside the if block is visible outside it?"
"Exactly. In JavaScript, var is function-scoped, not block-scoped. That if block doesn't create a new scope—only functions do. So message exists throughout the entire greet function."
"That's... weird. Languages like C and Java create a new scope for blocks."
"They do. JavaScript's var didn't. For twenty years, this was just how JavaScript worked. Then, in 2015, JavaScript got a major update called ES6. And it introduced let and const."
Margaret wrote another example:
function greet(name) {
if (name) {
let message = "Hello, " + name;
}
console.log(message); // ReferenceError: message is not defined
}
greet("Alice"); // Error!
"Now the variable is truly block-scoped," Margaret explained. "It exists only inside the if. Outside of it, message doesn't exist."
Timothy leaned back. "So let is just var but fixed?"
"Essentially, yes. let is what var should have been. It's block-scoped. It respects the boundaries of if statements, loops, and other blocks."
"Then why does anyone use var anymore?"
"Good question. They shouldn't, in new code. But millions of lines of JavaScript were written with var. And JavaScript never breaks old code. So var still exists, still works the old way, and still confuses people."
Three Ways to Declare (And Why)
Margaret pulled out three index cards and laid them on the desk.
"JavaScript gives you three ways to declare variables. Each one tells JavaScript something different about how you plan to use that variable."
Card One: var
var x = 5;
var x = 10; // Allowed - you can re-declare
x = 15; // Allowed - you can reassign
console.log(x); // 15
"With var, you can re-declare the same variable and reassign it. It's flexible. Flexible is good for quick scripts. Flexible is terrible for large programs, because you can accidentally overwrite a variable you didn't mean to touch."
Card Two: let
let y = 5;
// let y = 10; // NOT ALLOWED - can't re-declare
y = 15; // Allowed - you can reassign
console.log(y); // 15
"With let, you declare a variable once. You can change its value, but you can't accidentally re-declare it with let again. It's safer."
Card Three: const
const z = 5;
// const z = 10; // NOT ALLOWED - can't re-declare
// z = 15; // NOT ALLOWED - can't reassign
console.log(z); // 5
"With const, you declare a variable once, and it stays that value forever. It's the safest option."
Timothy studied the three cards. "So const is like Python's constants?"
"Similar idea, but not quite. In Python, constants are just a convention—a variable name in ALL_CAPS that programmers agree not to change. In JavaScript, const is enforced by the language. Try to change it, and you'll get an error."
"So we should always use const?"
Margaret leaned back. "Most JavaScript developers think so. The philosophy is: start with const. If you later realize you need to change the value, switch to let. And var... pretend it doesn't exist."
"Why not just always use const then?"
"Because sometimes you genuinely need to change a variable's value," Margaret said. She wrote:
// Counting through numbers
for (let i = 0; i < 10; i++) {
console.log(i);
}
// This won't work with const because i changes
// for (const i = 0; i < 10; i++) { // Error!
"Here, i starts at 0 and increases with each loop. If you declare it as const, you can't reassign it, and the loop breaks."
"So let is for things that change, const is for things that don't?"
"That's the pattern, yes. And it makes your code clearer. When someone reads const x = 5, they immediately know: this value never changes. When they read let y = 0, they know: this value might change as the program runs."
The Scope Chain
Timothy leaned forward. "But I still don't fully understand var. When I use it in a function and then an if block, where does it actually live?"
Margaret smiled. She loved when Timothy asked the right follow-up questions.
"That's the key insight. var creates variables in the function scope. Let me show you what I mean."
She wrote:
function example() {
console.log(typeof x); // undefined (not an error!)
if (true) {
var x = 5;
}
console.log(x); // 5
}
example();
"Notice that console.log(typeof x) prints undefined instead of throwing an error. Why? Because JavaScript moves all var declarations to the top of the function. This is called 'hoisting.'"
She rewrote it to show what JavaScript actually does internally:
function example() {
var x; // JavaScript moves this to the top
console.log(typeof x); // undefined - x exists but has no value
if (true) {
x = 5; // This just assigns to the already-declared x
}
console.log(x); // 5
}
example();
"So var declarations are 'hoisted' to the function scope, but their assignments stay where you wrote them. That's why you can access x before you've assigned it—it exists, but it's undefined."
Timothy blinked. "That's... really confusing."
"It is. And let doesn't do this. With let, variables don't exist until the line where you declare them."
function example() {
// console.log(y); // ReferenceError: y is not defined
let y = 5;
console.log(y); // 5
}
example();
"If you try to access y before its declaration, you get an error. Much clearer."
A Practical Example: The Counter
Margaret pulled up a new file. "Let's see all three in action with something practical. A simple counter function."
The var way (old, dangerous): ❌
function createCounter() {
var count = 0;
return function() {
count = count + 1;
console.log(count);
}
}
const counter1 = createCounter();
counter1(); // 1
counter1(); // 2
counter1(); // 3
"This works, but var makes it easy to accidentally overwrite count somewhere else in your code. And the scoping rules are confusing."
The let way (better): ✅
function createCounter() {
let count = 0;
return function() {
count = count + 1;
console.log(count);
}
}
const counter2 = createCounter();
counter2(); // 1
counter2(); // 2
counter2(); // 3
"With let, it's clearer that count is limited to the scope of this function. And you can't accidentally re-declare it."
The const way (best): ✅
function createCounter() {
const count = 0;
return function() {
count = count + 1; // ERROR! Can't reassign const
console.log(count);
}
}
const counter3 = createCounter();
counter3(); // TypeError: Assignment to constant variable
"Wait, that doesn't work," Timothy said. "We need to change count."
Margaret nodded. "Good observation. In this case, we have to use let because we're reassigning the variable. But there's another pattern that uses const effectively."
She rewrote it:
function createCounter() {
let count = 0;
return {
increment() {
count = count + 1;
},
get() {
return count;
}
};
}
const counter4 = createCounter();
counter4.increment();
counter4.increment();
console.log(counter4.get()); // 2
"Here, we return an object with methods. The object itself is const (it never changes), but the methods modify the count variable inside the closure. We get safety and functionality."
Timothy traced through the code carefully. "So count is hidden inside the function, and the only way to change it is through the methods on the returned object?"
"Exactly. This is called 'encapsulation.' The const keyword on the returned object means 'this object reference never changes.' But the methods on that object can modify things inside."
Scope and Blocks (Finally, let Makes Sense)
Timothy pulled up his original confusing example. "Now let me understand this with what we've learned."
var x = 5;
if (true) {
var x = 10;
}
console.log(x); // 10
"With var, both declarations are in the same scope—the global scope. So the second one overwrites the first."
Margaret nodded. "The if block doesn't create a new scope for var. So this is exactly the same as:"
var x = 5;
var x = 10; // Re-declaring the same variable
console.log(x); // 10
"Now the let version:"
let y = 5;
if (true) {
let y = 10; // New variable in the block's scope
}
console.log(y); // 5
"Here, the if block creates a new scope. The second let y = 10 creates a different y that only exists inside the block. Outside, the original y is still 5."
Timothy finally nodded. "So let respects block boundaries, and var ignores them."
"Precisely. And that's why modern JavaScript uses let and const. They give you scoping that actually makes sense."
Margaret stood and pulled up another example on Timothy's laptop.
"Before we finish, one more critical point about const that trips up beginners."
She wrote:
const myArray = [1, 2, 3];
myArray.push(4); // ✅ Allowed - modifying the array's contents
console.log(myArray); // [1, 2, 3, 4]
myArray = [5, 6, 7]; // ❌ Error: Assignment to constant variable
"Notice," Margaret said, "that you can modify what's inside the array. But you cannot reassign the variable itself. The const protects the binding, not the contents."
Timothy nodded slowly. "So const means 'this variable always points to the same object,' not 'this object never changes.'"
"Exactly right. It's a crucial distinction."
The Rule of Three
Margaret closed her notebook. "Here's what you need to remember going forward."
Use
constby default. Declare everything withconstunless you have a specific reason not to.Use
letwhen you need to reassign. If a variable's value changes after declaration, useletinstead.Never use
varin new code. It's for old JavaScript. You'll see it in legacy code, but pretend it doesn't exist.
Timothy wrote this down. "What about when I'm building actual applications? Will I run into edge cases?"
"Certainly. But for now, these three rules will serve you well. Follow them, and you'll avoid the most common mistakes that trip up JavaScript beginners."
She stood and walked back to her desk. "Tomorrow, we'll look at something that seems equally simple: the this keyword. And you'll discover that JavaScript's relationship with this is even more confusing than variables."
Timothy groaned. "Even worse?"
Margaret smiled over her shoulder. "Oh, Timothy. You have no idea."
Key Takeaways
varis function-scoped — it can be accessed anywhere in its function, and declarations are hoisted to the top.letis block-scoped — it only exists within the block where it's declared (if, loop, function, etc.).constprevents reassignment — the variable's value cannot change once assigned.All three declare variables — the difference is in where they exist and whether they can change.
Hoisting affects
var— declarations move to the top of the function, but assignments stay in place.letandconstare hoisted but not initialized — They're hoisted to the top of their block scope but live in a "Temporal Dead Zone" until their declaration line. Accessing them before declaration throws aReferenceError.Modern JavaScript prefers
constandlet—varexists only for backward compatibility.Block scope is safer — it prevents accidental overwrites and makes code easier to reason about.
constprotects the binding, not the content —constprevents variable reassignment, but objects and arrays can still be modified internally.const arr = [1]; arr.push(2)works, butarr = [3]fails.Scope creates encapsulation — functions with
letandconsthide variables from the outside world.
Discussion Questions
Why do you think JavaScript created
varthis way instead of block-scoping from the start?When would you choose
letoverconst, and why?What's the difference between "can't reassign" (
const) and "is immutable"?How does hoisting change the way you think about variable declarations?
In the counter example, why did the object pattern with methods provide better encapsulation than just using
let?
Share your explorations with variables in the comments below!
About This Series
The Secret Life of JavaScript reveals how JavaScript actually works—not how you wish it worked. Through the conversations of Timothy (a curious developer learning his first new language) and Margaret (a wise JavaScript expert) in a Victorian library in London, we explore the ideas beneath JavaScript's quirky syntax.
Each article stands alone but builds toward deeper understanding. Whether you're new to JavaScript or just curious about why it works the way it does, this series illuminates the path.
Coming next: "The Secret Life of JavaScript: Understandingthis" — where context becomes chaos, and you'll finally understand why this doesn't always mean what you think it means.
Aaron Rose is a software engineer and technology writer at tech-reader.blog and the author of Think Like a Genius.
Top comments (6)
The use of
const,letandvarin the post is the common agreement in the community.I think the biggest pain point is the use of
const. While it is a constant for primitives like strings and floats, when you are using it on arrays of objects it is not that much of a constant.For me working with objects or arrays I use
letwhen the variable should not be exposed. Otherwise I usevar.The reason block scopes are safer from overwriting has to do with the fact that javascript has no namespacing build in. The emulation of namespaces is nesting variables in objects.
Thanks David! You've identified the issue that trips people up—const protects the binding, not the contents. Your point about arrays and objects is perfect. The namespacing insight about block scope is excellent too—that's the real nice thing with let and const. Cheers buddy! ❤
Neatly written
thanks! cheers Abhinav ❤
Thanks for this clarification!
My pleasure, Laurent! I'm glad it landed clearly. This series is all about making the confusing parts of JavaScript actually make sense. More chapters coming on this, prototypes, and the event loop. Thanks for reading! ❤