const does not mean "constant value" the way you might expect, and what it actually protects;var behaves so strangely compared to let and const;.js file with node.Learn JS Series):Before we start, here are worked solutions to the three exercises from last time. Do compare them with your own attempts, the differences are where the learning hides.
Exercise 1 - seconds in a week, in one expression:
console.log(60 * 60 * 24 * 7); // 604800
The key insight: JavaScript evaluates arithmetic left to right with normal precedence, so you can chain multiplications freely. Sixty seconds times sixty minutes times twenty-four hours times seven days is 604800.
Exercise 2 - an about.js using two consts and a template literal:
const firstName = "scipio";
const favouriteNumber = 7;
console.log(`Hi, I am ${firstName} and my favourite number is ${favouriteNumber}.`);
The insight: a template literal (backticks) lets you drop values straight into text with ${...}, so you never have to glue strings with + and worry about spaces.
Exercise 3 - watching a type change at runtime:
let value = 100;
console.log(typeof value); // "number"
value = "one hundred";
console.log(typeof value); // "string"
The insight: in JavaScript the variable has no fixed type, only the value it currently holds does. In a language like C the variable's type is fixed at compile time; in JavaScript it is a label you can move onto any kind of value.
Right, on to today's topic, which is the foundation under all of that: how we actually declare variables, and why the choice of keyword matters more than beginners expect.
JavaScript gives you three ways to declare a variable: const, let, and the older var. My recommendation up front, so you are never confused: use const by default, use let when you genuinely need to reassign, and avoid var entirely in new code. The rest of this episode explains why that is the right rule, because a rule you understand is a rule you will actually follow.
Let's start with let, the general-purpose one:
let score = 0;
score = 10; // fine, we can reassign
score = score + 5;
console.log(score); // 15
A let binding can be reassigned as often as you like. The name score is a box, and you are free to put a new value in that box whenever you want. Now const:
const pi = 3.14159;
// pi = 3; // ERROR (runtime): Assignment to constant variable.
console.log(pi);
A const binding cannot be reassigned. Try it and Node throws TypeError: Assignment to constant variable. That single guarantee is why we prefer const: when you read const pi = 3.14159, you know for certain that pi still means that value fifty lines later. It removes a whole category of "wait, who changed this?" bugs, and it makes code dramatically easier to read, because every const is a small promise the code keeps for you.
Here is a way to think about it that has served me well for years. Reaching for let is telling the reader "this value is going to change, keep an eye on it". Reaching for const is telling the reader "relax, this one is settled". Most values in a well-written program never actually change after they are set, so most declarations should be const. When you find yourself typing let, pause for half a second and ask whether you truly need to reassign. Quite some of the time you do not, and const was the better choice.
Here is the part that trips up almost everyone, so read it twice. const stops you from reassigning the variable. It does not stop you from changing the contents of the thing the variable points to, if that thing is an object or an array.
const user = { name: "scipio", level: 1 };
user.level = 2; // allowed! we mutate the object, not the binding
user.name = "scipio_nl"; // also allowed
console.log(user); // { name: 'scipio_nl', level: 2 }
// user = { name: "someone" }; // ERROR (runtime): this WOULD reassign the binding
So user.level = 2 is fine because we are changing a property inside the object. But user = {...} would be pointing user at a brand new object, which const forbids. The same is true for arrays:
const numbers = [1, 2, 3];
numbers.push(4); // allowed, we mutate the array
console.log(numbers); // [1, 2, 3, 4]
// numbers = [9, 9, 9]; // ERROR (runtime): reassigning the binding is not allowed
The mental model: a const is a name glued permanently to one value. If that value is a number or string, it truly never changes (numbers and strings are immutable in JavaScript, more on that in a later episode). If that value is an object or array, the name stays glued to that same object, but the object's insides can still change. We will go much deeper into this value-versus-reference distinction later, it is one of the most important ideas in the whole language, and half of the confusing bugs beginners hit come from not having it straight.
If you genuinely want to freeze the contents too, JavaScript gives you Object.freeze:
const config = Object.freeze({ retries: 3, timeout: 1000 });
config.retries = 99; // ignored (throws in strict mode / modules)
console.log(config.retries); // 3 -- the object itself is now locked
Object.freeze is a runtime lock on the object's contents, which is a different mechanism from const. const locks the binding (the name to the value); Object.freeze locks the value's properties. They are independent tools, and it is worth keeping the two ideas separate in your head. Nota bene: freeze is shallow, it only locks the top level, so a nested object inside config could still be mutated. We will come back to that when we do objects properly.
var is the original keyword from 1995, and it has two behaviours that modern JavaScript deliberately moved away from. You will still see it in older code and in a lot of answers on the web, so you need to recognise it, but you should not write new code with it.
First, var is function-scoped, not block-scoped. A variable declared with let or const only exists inside the nearest set of curly braces { }. A var ignores block braces and leaks out to the whole surrounding function:
{
let blockScoped = "inside";
var functionScoped = "leaks out";
}
console.log(functionScoped); // "leaks out" -- var escaped the block
// console.log(blockScoped); // ERROR (runtime): blockScoped is not defined
That leaking behaviour causes real bugs, especially inside loops and if blocks, where you expect a variable to stay local but it does not. The classic example is a loop that captures its counter. Do not sweat the mechanics of the inner functions yet (we do closures properly later), just watch what value each one reports:
var fns = [];
for (var i = 0; i < 3; i++) {
fns.push(function () { return i; });
}
console.log(fns[0](), fns[1](), fns[2]()); // 3 3 3 -- surprise!
Every function printed 3, not 0 1 2, because with var there is only one shared i for the whole loop, and by the time the functions run the loop has finished and left i at 3. Now swap var for let and the bug simply vanishes:
var fns2 = [];
for (let j = 0; j < 3; j++) {
fns2.push(function () { return j; });
}
console.log(fns2[0](), fns2[1](), fns2[2]()); // 0 1 2 -- fixed
With let, each turn of the loop gets its own fresh j, so each captured function remembers its own number. This one difference has saved beginners from countless hours of confusion, and it is a big part of why let exists at all.
Second, var is hoisted in a way that lets you use it before you declare it, silently giving you undefined in stead of an error:
console.log(sneaky); // undefined -- no crash, which is worse
var sneaky = "here now";
console.log(sneaky); // "here now"
Reading a variable before it is defined should be an error, not a silent undefined. A silent undefined tends to travel deep into your program before it finally causes a crash somewhere far away from the real mistake, which makes debugging miserable. And with let and const, this is an error right at the point of misuse, which brings us to hoisting properly.
Hoisting is JavaScript's behaviour of moving declarations to the top of their scope before running the code. All three keywords are hoisted, but they behave differently once hoisted. With var, the variable is hoisted and immediately initialized to undefined, which is exactly why the sneaky example above did not crash, it genuinely existed (as undefined) from the top of the function.
With let and const, the variable is hoisted but not initialized. From the top of the block until the line where you declare it, the variable exists but is off-limits. This zone is called the Temporal Dead Zone (TDZ), and touching the variable there is an error:
// console.log(early); // ERROR (runtime): Cannot access 'early' before initialization
const early = "value";
console.log(early); // "value"
This is a feature, not an annoyance. It means let and const catch the mistake of using a variable too early, in stead of handing you a silent undefined like var does. The TDZ turns a whole class of "why is this undefined?" mysteries into a clear, loud error message that points at the exact line. We will devote a whole episode to scope and the TDZ soon, because it explains a surprising amount of JavaScript's behaviour, but for now the practical takeaway is simple: declare your variables before you use them, and let/const will keep you honest.
The hard rules first. A variable name can contain letters, digits, $, and _, but cannot start with a digit. Names are case-sensitive, so count and Count are two different variables. And you cannot use reserved words like const, return, or class as names, because the parser needs those words for the language itself.
let userCount = 5; // fine
let $price = 10; // fine, $ is a legal character
let _internal = 1; // fine, _ is legal too
// let 2fast = 1; // ERROR: cannot start with a digit
// let class = "A"; // ERROR: reserved word
Then the conventions, which the whole JavaScript world follows even though the language does not force them:
const userName = "scipio"; // camelCase for variables and functions
const MAX_RETRIES = 5; // UPPER_SNAKE_CASE for true constants
const $element = "reserved-ish"; // $ is legal, often used by libraries
Use camelCase for ordinary variables (first word lowercase, later words capitalized). Reserve UPPER_SNAKE_CASE for values that are fixed configuration, like a maximum count or a magic number you want to name. Do not fight these conventions, following them makes your code instantly readable to every other JavaScript developer on the planet, and going against them just makes people wonder what you were thinking.
One more practical tip that matters more than any rule: give things descriptive names. const secondsPerWeek = 604800 tells the reader far more than const x = 604800. You are writing for the human who reads this later, and that human is very often you, six months from now, having completely forgotten how any of it worked. Good names are the cheapest documentation there is.
A lot of you arrived here from the Learn Python Series, so a look sideways sharpens the picture of what JavaScript actually chose here, because every language makes a slightly different bet.
Python does not have const at all. Every name can be rebound at any time, and the "constant" is purely a convention between humans:
# Python has no const. A name can always be rebound.
PI = 3.14159 # UPPER_CASE says "please treat this as constant"...
PI = 3 # ...but Python happily allows this anyway
So Python trusts you completely and enforces nothing. JavaScript's const is a real, enforced promise, which sits a notch stricter than Python. Rust goes further still and flips the default the other way around, bindings are immutable unless you explicitly opt in to mutability with mut:
// Rust: bindings are immutable by DEFAULT.
// let score = 0;
// score = 10; // ERROR: cannot assign twice to immutable variable
// let mut score = 0; // you must opt IN to reassignment with `mut`
// score = 10; // now this is allowed
That is basically JavaScript's const-by-default advice, except Rust bakes it into the language as the actual default, and you type extra to get the mutable version. Go sits somewhere in between: it has const, but only for compile-time constant values like numbers, strings and booleans, not for the flexible "any value" constants JavaScript allows:
// Go: const is only for compile-time constants.
// const pi = 3.14159 // fine
// const list = []int{} // ERROR: a slice can't be a Go const
So the spectrum runs like this: Python enforces nothing (convention only), JavaScript enforces the binding at runtime with const, Go enforces a narrower compile-time const, and Rust makes immutability the default and mutability the opt-in. Knowing where JavaScript sits helps you understand why the advice is "prefer const", it is the language nudging you toward the safer, Rust-like habit without forcing it. And once you internalize that habit, moving to a stricter language later feels natural in stead of annoying.
Rules are easier to trust once you see them working in something slightly more real than a one-liner. Here is a tiny scoring loop that uses each keyword exactly where it belongs, so you can see the reasoning in context:
const MAX_SCORE = 100; // a true, fixed constant -- UPPER_SNAKE_CASE
const player = { name: "ada", score: 0 }; // the binding is fixed, the object mutates
for (const point of [10, 25, 40]) { // `point` never changes inside one turn -> const
player.score = player.score + point; // mutate the object's property, allowed under const
}
let status = "playing"; // this genuinely changes, so `let` is honest here
if (player.score >= MAX_SCORE) {
status = "won";
}
console.log(`${player.name} has ${player.score} points and is ${status}.`);
// ada has 75 points and is playing.
Look at each choice. MAX_SCORE is a fixed number, so it is a const in shouting case. player is a const too, even though its score keeps changing, because the binding never moves to a different object, only the object's insides update. Inside the loop, point is declared const because within a single turn of the loop it is never reassigned, and, as we saw earlier, each turn gets its own fresh binding anyway. Only status is a let, because it truly flips from one value to another depending on the outcome. That is the whole discipline in one snippet: reach for const first, and let the rare genuinely-changing values earn their let. Do this for a week and it becomes automatic, and your code starts documenting its own intentions for free.
const array of three of your favourite words. Then use .push() to add a fourth word and print the array. Explain in one sentence why this does not violate const.console.log a const variable one line before you declare it. Read the exact error message and write it down.total is 6, using the right keyword: const total = 0; for (let n of [1,2,3]) { total = total + n; } console.log(total);. Explain which keyword you changed and why.const by default, let when you must reassign, and avoid var in new code.const prevents reassigning the binding, but does NOT freeze the contents of an object or array it points to. Use Object.freeze for that (and remember it is shallow).var is function-scoped and leaks out of blocks, and its single shared loop variable causes classic bugs; let and const are block-scoped and give each loop turn its own binding.let/const sit in a Temporal Dead Zone until their line runs, turning "used too early" into a clear error in stead of a silent undefined.camelCase for variables, UPPER_SNAKE_CASE for fixed constants, and descriptive names always.const-by-default habit is the sensible middle ground.Next episode we slow right down and look at the raw material every program manipulates: JavaScript's primitive types, all seven of them, and the surprising things hiding inside "just a number" and "just a string". Get const into your fingers first, though, because we will be leaning on it constantly from here on ;-)
See you in the next episode.