JavaScript var — Hoisting Broke Our Payment Flow
Intermittent payment failures: 20% of checkout sessions showed NaN totals because var ignored block scope.
20+ years shipping production JavaScript and front-end systems at scale. Everything here is grounded in real deployments.
- Variables store data and let you retrieve it by name
- var is function-scoped and hoisted as undefined — avoid in new code
- let is block-scoped with Temporal Dead Zone — throws ReferenceError on early access
- const is block-scoped, cannot be reassigned, but object/array contents remain mutable
- Use const by default, let when reassignment is needed, var only for legacy code
- Biggest mistake: assuming const makes objects fully immutable — only the reference is locked
In JavaScript, a variable is a named container that stores a value in memory, created using var, let, or const. The core problem var introduces is hoisting—JavaScript's behavior of moving variable declarations to the top of their scope before execution.
This means var declarations are accessible before their actual line of code, but with an initial value of undefined, not the assigned value. This subtlety can cause silent failures in production, like a payment flow where a variable used for transaction validation evaluates to undefined instead of the expected amount, leading to incorrect charges or failed payments. let and const fix this with block scoping and the Temporal Dead Zone (TDZ), which throws a ReferenceError if you access them before declaration, making bugs impossible to ignore. const further prevents reassignment, but its immutability is shallow—object properties and array elements can still be mutated, a common gotcha when you assume a frozen state.
Understanding the difference between undefined (a declared but unassigned variable) and undeclared (a variable that doesn't exist, causing a ReferenceError) is critical: var hoisting can mask undeclared variables as undefined, leading to runtime crashes when you try to use them in critical logic. In the ecosystem, var is legacy; modern codebases (React, Node.js, TypeScript) default to let and const for predictability.
Avoid var in new code, and refactor it out of existing systems—especially in financial or state-sensitive applications—to prevent hoisting-induced bugs that are notoriously hard to trace.
Think of a variable as a labelled box on a shelf. You write a name on the outside of the box, put something inside it, and whenever you need that thing you just grab the box by its label. In JavaScript, 'var', 'let', and 'const' are simply three different types of boxes — they differ in the rules about where you're allowed to use them and whether you can swap out what's inside. That's it. Once that clicks, the rest is just details.
Every program you've ever used — a weather app, a game, a shopping cart — needs to remember things while it's running. It needs to know the current temperature, your score, how many items are in your basket. Variables are how programs remember things. Without them, your code would be a one-shot firework: bright for a second, then gone, with nothing to show for it. In JavaScript specifically, variables are the very first building block you'll write in almost every line of real code.
What a Variable Actually Is — And How to Create One
A variable is a named slot in your computer's memory where you can store a value and retrieve it later by name. You create one using a keyword (var, let, or const), then a name you choose, then optionally an equals sign and an initial value.
The name you choose is completely up to you — but good names describe what the value represents. 'playerScore' is miles better than 'ps', because when you read it three weeks later you'll still know what it means.
JavaScript is case-sensitive, so 'playerScore' and 'PlayerScore' are two completely different variables. The convention in JavaScript is camelCase — first word lowercase, every word after that starts with a capital letter. 'userName', 'totalPrice', 'isLoggedIn' — that's the style professional developers use and that you should adopt from day one.
Once a variable holds a value, you can read that value, change it, pass it to a function, or do maths with it. It's just a labelled box — incredibly simple, but the foundation of everything.
// --- Creating variables with let (the modern default choice) --- // Declare a variable and assign a value to it in one line let playerName = "Alex"; // Declare a variable now, assign a value later let playerScore; playerScore = 0; // we set it here when we're ready // Read the value by using the variable name console.log(playerName); // prints: Alex console.log(playerScore); // prints: 0 // --- Update the value stored inside the variable --- playerScore = 150; // Alex just scored 150 points console.log(playerScore); // prints: 150 // --- JavaScript can store many types of value --- let gameTitle = "Space Quest"; // text (called a 'string') let numberOfLives = 3; // a number let isGameOver = false; // true or false (called a 'boolean') console.log(gameTitle, numberOfLives, isGameOver); // prints: Space Quest 3 false
var, let, and const — The Differences That Actually Matter
JavaScript has three keywords for declaring variables, and they behave differently in two important ways: scope (where the variable can be seen and used) and mutability (whether the value can be changed after it's set).
'var' is the original keyword from 1995. It's function-scoped — meaning it's visible anywhere inside the function it was created in. It also gets 'hoisted', which causes some truly weird bugs we'll get to shortly. Avoid var in new code.
'let' was introduced in 2015 and is block-scoped — it only exists inside the curly braces {} where it was declared. This is far more predictable and is your go-to for any value that will change over time.
'const' is also block-scoped, but with one extra rule: once you assign a value to a const variable, you can't reassign it. Think of const as a box that's been glued shut — you can look inside, but you can't put something different in. Use const as your default choice. Only switch to let when you know the value needs to change.
// ============================================================ // PART 1 — const: for values that should never change // ============================================================ const maxLivesAllowed = 3; // this rule never changes in our game console.log(maxLivesAllowed); // prints: 3 // Trying to change a const causes an error: // maxLivesAllowed = 5; // TypeError: Assignment to constant variable. // ============================================================ // PART 2 — let: for values that will change over time // ============================================================ let currentLives = 3; // starts at 3 console.log(currentLives); // prints: 3 currentLives = currentLives - 1; // player lost a life console.log(currentLives); // prints: 2 // ============================================================ // PART 3 — Block scope with let and const // The {} curly braces create a 'block' // Variables declared inside cannot be seen outside // ============================================================ let roundNumber = 1; if (roundNumber === 1) { let roundMessage = "Welcome to Round 1!"; // only lives inside this block console.log(roundMessage); // prints: Welcome to Round 1! } // console.log(roundMessage); // ReferenceError: roundMessage is not defined // It no longer exists out here — the block ended // ============================================================ // PART 4 — var ignores block boundaries (this is the problem) // ============================================================ if (true) { var leakyVariable = "I escaped the block!"; // var ignores {} scope } console.log(leakyVariable); // prints: I escaped the block! // ^ This is unexpected behaviour — var 'leaked' out of the if block // This is why modern JavaScript uses let and const instead
Hoisting — The Weird var Behaviour That Trips Everyone Up
Hoisting is one of JavaScript's most surprising quirks, and it's the main reason var fell out of favour. When JavaScript reads your file before running it, it 'hoists' — meaning it mentally moves — all var declarations to the top of their function. The declaration moves up, but the value assignment stays where you wrote it.
The practical effect: you can reference a var variable before the line where you wrote it, and instead of crashing, JavaScript quietly gives you 'undefined'. That silent failure is dangerous because your code keeps running with broken data.
let and const are also technically hoisted, but they are placed in a 'Temporal Dead Zone' (TDZ) — if you try to access them before their declaration line, you get a clean, clear ReferenceError that tells you exactly what went wrong. That's far better than a mystery 'undefined'.
Hoisting is something interviewers love asking about, so understanding the difference between var's silent undefined and let/const's noisy ReferenceError is worth burning into your memory.
// ============================================================ // PART 1 — var hoisting: silent and confusing // ============================================================ console.log(welcomeMessage); // prints: undefined (NOT an error!) // ^ JavaScript hoisted the *declaration* of welcomeMessage to the top // but the *value* "Hello!" hasn't been assigned yet at this point var welcomeMessage = "Hello!"; console.log(welcomeMessage); // prints: Hello! // What JavaScript actually sees (after hoisting) is: // var welcomeMessage; <-- moved to top automatically // console.log(welcomeMessage); <-- undefined, not assigned yet // welcomeMessage = "Hello!"; <-- value assigned here // console.log(welcomeMessage); <-- Hello! // ============================================================ // PART 2 — let hoisting: loud and helpful // ============================================================ // Uncommenting the next line would cause: // ReferenceError: Cannot access 'errorMessage' before initialization // console.log(errorMessage); let errorMessage = "Something went wrong"; console.log(errorMessage); // prints: Something went wrong // let is in the Temporal Dead Zone before this line — // any access before here throws an error immediately // ============================================================ // PART 3 — A realistic hoisting bug with var // ============================================================ function checkHighScore(newScore) { if (newScore > 100) { var congratsText = "New high score!"; } // var leaks out of the if block — it's now undefined here, not missing console.log(congratsText); // prints: undefined (silent bug!) } checkHighScore(50); // score is NOT > 100, so congratsText was never set // But JavaScript doesn't throw an error — it just says undefined
const With Objects and Arrays — The Gotcha You Need to Know
Here's something that surprises even experienced developers. When you use const with an object or an array, it doesn't make the contents frozen — it only prevents you from pointing the variable at a completely different object or array.
Think of it like this: const means the label on your box is permanently attached. You can still reach inside the box and rearrange what's in it — you just can't pick up the label and stick it on a different box.
This means you can add properties to a const object, update them, and delete them. The same goes for arrays — you can push, pop, and sort. What you cannot do is say 'now this variable points to a brand new object'.
This trips up almost every beginner who first hears 'const means it can't change' — that statement is only partially true. The binding is constant; the contents are not. If you truly need to freeze an object so nobody can change its contents, JavaScript has a built-in method for that: Object.freeze().
// ============================================================ // PART 1 — const object: the binding is locked, not the contents // ============================================================ const userProfile = { username: "alex_42", level: 5, }; console.log(userProfile.username); // prints: alex_42 // You CAN change properties inside the object userProfile.level = 6; // user levelled up! userProfile.badges = ["explorer"]; // add a new property console.log(userProfile); // prints: { username: 'alex_42', level: 6, badges: [ 'explorer' ] } // You CANNOT reassign the variable to a new object: // userProfile = { username: "someone_else" }; // TypeError: Assignment to constant variable. // ============================================================ // PART 2 — const array: same rule applies // ============================================================ const inventoryItems = ["sword", "shield"]; inventoryItems.push("potion"); // adding to the array is allowed console.log(inventoryItems); // prints: [ 'sword', 'shield', 'potion' ] // inventoryItems = ["axe"]; // TypeError — can't swap the whole array // ============================================================ // PART 3 — Object.freeze() if you truly want immutability // ============================================================ const gameConfig = Object.freeze({ maxPlayers: 4, startingLives: 3, }); gameConfig.maxPlayers = 10; // silently fails in non-strict mode console.log(gameConfig.maxPlayers); // still prints: 4 // Object.freeze() makes the contents truly read-only
Object.freeze() for configuration objects, and enforce it with a type checker or unit test.Object.freeze() or a library like Immer.Block Scoping and the Temporal Dead Zone — Why let and const Are Safer
The Temporal Dead Zone (TDZ) is the period between the start of a block and the line where a let or const variable is declared. During this zone, any access to that variable throws a ReferenceError. This is a feature, not a bug — it catches mistakes early.
Consider this: with var, you can read undefined before the declaration line, and the code continues running silently. With let, the engine stops immediately and tells you exactly which variable you tried to access too early. That's the difference between a debugging session and a production incident.
Block scoping means each pair of curly braces creates a new scope. Variables declared with let or const inside that block are forever invisible to the outside. This allows you to reuse variable names safely in different blocks without collision — a common source of confusion with var.
The combination of block scoping and TDZ makes modern JavaScript predictable. You can reason about where a variable exists and whether it has a value, simply by reading the structure of the code.
// ============================================================ // PART 1 — Temporal Dead Zone in action // ============================================================ { // TDZ starts here for myLet // console.log(myLet); // ReferenceError: Cannot access 'myLet' before initialization let myLet = "after TDZ"; console.log(myLet); // prints: after TDZ } // ============================================================ // PART 2 — Block scope prevents variable leaking // ============================================================ { let blockOnly = "inside"; console.log(blockOnly); // prints: inside } // console.log(blockOnly); // ReferenceError: blockOnly is not defined // ============================================================ // PART 3 — Safer loops with let (classic var bug) // ============================================================ for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); // prints: 3, 3, 3 } for (let j = 0; j < 3; j++) { setTimeout(() => console.log(j), 0); // prints: 0, 1, 2 } // let creates a new binding for each iteration, var does not
- The TDZ exists from block start to the line where let/const is declared.
- Any access during TDZ throws ReferenceError immediately — no silent undefined.
- var has no TDZ — the declaration is hoisted, so you get undefined.
- The TDZ makes bugs loud and easy to catch during development.
- Think of const/let as 'declare first, then use' — reversing the order triggers the alarm.
Undefined vs. Undeclared — The Two-State Lie That Kills at Runtime
There are exactly two ways a variable can be 'not a value' in JavaScript, and confusing them has burned more devs than any framework churn.
An undeclared variable is one that was never created with var, let, or const. You try to read it and the runtime screams ReferenceError: x is not defined. This is a hard stop. Your app crashes. No recovery. This is the bug you introduce when you mistype a variable name or forget to declare it before a tight loop.
An undefined variable exists in the scope, but its value is the primitive undefined. This happens when you declare without assigning (let x;), or when a function returns nothing. It's not a crash by itself — it's a silent landmine. undefined + 1 gives NaN. undefined.property throws. The garbage flows downstream.
Production rule: never rely on undefined as a meaningful state. If a variable must eventually hold a value, initialize it to null explicitly. null means 'intentionally empty'. undefined means 'I forgot'. Read that difference again.
// io.thecodeforge — javascript tutorial // Undeclared — instant crash console.log(nonExistentVar); // ReferenceError: nonExistentVar is not defined // Declared but undefined let userInput; console.log(userInput); // undefined // The silent killer let cartTotal; function addItem(price) { cartTotal += price; // undefined + 10 = NaN } addItem(10); console.log(cartTotal); // NaN // The fix: explicit initialization let cartTotalSafe = 0; function addItemSafe(price) { cartTotalSafe += price; } addItemSafe(10); console.log(cartTotalSafe); // 10
undefined, not null. If you call fetchData(id) without passing id, that function runs with undefined. Always use default parameters: function fetchData(id = null).Variable Naming — The Convention That Saves Your Monday Morning
You can name a variable almost anything in JavaScript — letters, $, _, numbers (not at the start). But 'can' and 'should' are different oceans. Production code is read dozens of times more often than it's written. Name things so that the next person (or you, after a weekend) can understand intent in two seconds.
The hard rules: names are case-sensitive (user and User are different). You cannot use reserved keywords (class, return, typeof — yes, including undefined). Don't fight this. Fight the naming conventions that cause confusion.
Conventions that stick: camelCase for variables and functions (userSessionToken), PascalCase for constructors and classes (SessionManager), UPPER_SNAKE_CASE for true constants (MAX_RETRY_COUNT). The last one is a signal: 'this value is baked at build time, do not mutate me.'
One letter variable names? Fine in a five-line loop counter (i, j). In any other context you're creating a puzzle. data, info, val — these are placeholders, not names. Be specific. userList over list. paymentStatus over status. Your future self will send you a thank-you note.
// io.thecodeforge — javascript tutorial // Bad naming — requires mental parsing let a = 1; // what is a? loop index? count? let d = new Date(); let x = 'pending'; // what state does this represent? // Good naming — intent is explicit let connectionRetryCount = 1; let lastPollTimestamp = new Date(); let orderStatus = 'pending'; // True constant — never reassign const API_TIMEOUT_MS = 5000; const VALID_ROLES = ['admin', 'editor']; // Read this after a weekend off — which block makes sense faster?
Summary
Variables in JavaScript are containers that store data values, but how you declare them determines everything about their behavior — from memory allocation to runtime safety. The three declaration keywords — var, let, and const — differ fundamentally in scoping, hoisting, and reassignment rules. var is function-scoped and hoisted with an undefined default, making it prone to subtle bugs. let and const are block-scoped and subject to the temporal dead zone, ensuring variables aren't accessed before their declaration. const prevents reassignment but allows mutation for objects and arrays. These distinctions directly impact code predictability, debugging, and team collaboration. Choosing let or const as defaults eliminates an entire class of bugs that var introduces, especially in large codebases. Understanding these mechanics isn't academic — it's the foundation for writing JavaScript that behaves as expected across functions, loops, and conditionals.
// io.thecodeforge — javascript tutorial // Summary: variable behavior at a glance let x = 10; // block-scoped, no hoisting access const y = { value: 20 }; // mutable, not reassignable console.log(x + y.value); // 30
var in loops or callbacks can cause unexpected shared state. Always prefer let for counters.let and const, not var, to avoid scoping bugs.Example: Demonstrating the Identifiers
This example shows how var, let, and const behave differently in the same context — a loop inside a function. var leaks out of the block, let stays contained, and const prevents any reassignment attempt. Run this to see how hoisting and block scoping affect output order and error behavior. The key insight: var declares the variable once per function, while let creates a new binding each iteration. This is why let is safer in loops — each closure captures the correct value, not the final one.
// io.thecodeforge — javascript tutorial // Demonstrates var, let, const differences for (var v = 0; v < 3; v++) {} console.log(v); // 3 (leaked) for (let l = 0; l < 3; l++) {} // console.log(l); // ReferenceError: l is not defined const c = 5; // c = 6; // TypeError: Assignment to constant variable console.log(c); // 5
var in a for-loop's condition block pollutes the function scope. Use let to avoid accidental reuse.let and const for block-scoped precision; var is an anachronism that breaks enclosure.The Silent Undefined That Broke a Payment Flow
- Never use var in new code — it's the leading cause of unexpected undefined values in production.
- Always prefer const by default; switch to let only when you know the value needs to change.
- Enable linting rules (e.g., no-var) in your CI pipeline to catch var declarations before they ship.
Object.freeze() or a library like Immer. Check for property reassignments or mutations elsewhere in the code.console.log('Before:', myVar); console.log('After:', myVar); // wrap around the assignmentAdd a debugger statement just before the assignment to step through scope.console.log('Access point:', lineNumber); // identify the line numberRun the code with breakpoints at the access and the declaration to confirm order.Object.freeze(obj) after creation to test if mutations cause errors in strict mode.Use Object.getOwnPropertyDescriptor(obj, prop) to see if property is writable.Object.freeze() or restructure to avoid mutation.| Feature | var | let | const |
|---|---|---|---|
| Introduced in | ES1 (1995) | ES6 (2015) | ES6 (2015) |
| Scope | Function scope | Block scope {} | Block scope {} |
| Can be reassigned? | Yes | Yes | No (primitives) |
| Can be redeclared? | Yes (dangerous) | No — throws SyntaxError | No — throws SyntaxError |
| Hoisting behaviour | Hoisted as undefined | Hoisted but in TDZ — throws ReferenceError | Hoisted but in TDZ — throws ReferenceError |
| Must be initialised? | No | No | Yes — must assign on declaration |
| Use today? | Avoid in new code | Yes — for changing values | Yes — default choice |
Key takeaways
Object.freeze() for true deep immutability.Common mistakes to avoid
4 patternsUsing var instead of let or const
Declaring a const variable without initialising it
Assuming const makes an object fully immutable
Object.freeze() if you genuinely need read-only contents, or use a library like Immer for deep immutability in larger apps.Using var inside a for loop and expecting correct closure values
Interview Questions on This Topic
What is the difference between var, let, and const in JavaScript? Can you explain scope and hoisting in your answer?
What happens if you try to access a let variable before it is declared? How does that differ from var?
If I declare 'const user = { name: "Sam" }' and then write 'user.name = "Alex"', does that throw an error? Why or why not?
Object.freeze() is used). So updating user.name is allowed. The common misconception is that const makes the value immutable, but it only locks the binding.Explain the Temporal Dead Zone and why it's considered a feature rather than a bug.
Frequently Asked Questions
Use const as your default for every new variable. If you later realise the value needs to change (like a counter or a flag that flips), switch it to let. Avoid var entirely in modern JavaScript — it has confusing scoping and hoisting behaviour that let and const fix cleanly.
It means JavaScript hoisted the variable declaration to the top of the function before running your code, but the value assignment hasn't happened yet at the point you're logging it. The variable exists (so no error), but nothing has been put inside it yet. This is one of the key reasons to avoid var — it fails silently instead of telling you what went wrong.
Yes, you can. const only prevents you from reassigning the variable to a completely new value — it doesn't freeze what's inside. So you can push items into a const array, update properties on a const object, and so on. If you need the contents to be truly unchangeable, wrap the object in Object.freeze() at the point of creation.
The Temporal Dead Zone (TDZ) is the time between entering a block and the execution of the let/const declaration line. During the TDZ, the variable exists but cannot be accessed — any attempt throws a ReferenceError. It exists to catch programming errors early. Without it, code could accidentally use a variable before it's initialised, leading to hard-to-find bugs (like var's undefined).
20+ years shipping production JavaScript and front-end systems at scale. Everything here is grounded in real deployments.
That's JS Basics. Mark it forged?
7 min read · try the examples if you haven't