JAVASCRIPT / VARIABLES: VAR, LET, AND CONST
Declaring variables with var, let, and const
Declare JavaScript variables with var, let, and const, choose the right keyword, and tell an unset binding from one that was never declared.
What you will learn
- Write var, let, and const declarations with or without an initial value
- Explain why let x; is legal but const x; is a SyntaxError
- Declare several names in one statement without creating an undeclared global
- Distinguish undefined (declared, unset) from a ReferenceError (never declared)
Understanding Declaring variables with var, let, and const
A declaration is the statement that introduces a name into a scope: a keyword, the name, and optionally an equals sign plus a starting value. Picture the name as a label attached to a slot the engine sets up when it enters that scope; every later mention of the name reads or writes that slot. JavaScript has three declaration keywords because the language grew in stages: var has been there since 1995, while let and const arrived in ES2015 to fix problems var caused. All three produce a binding you use identically; what differs is where the name is visible and whether you may point it at a different value later.
Declaring and initializing are two separate acts, and that is the idea to get straight before anything else. Writing let total; declares the name and the engine puts undefined in the slot, so you can supply the real value on a later line or inside a branch. const cannot work that way, because it fixes what the name points at the moment the binding is created, so the language demands the value up front and treats const total; as invalid syntax rejected while the file is parsed rather than when the line runs. The question to ask before typing the keyword is therefore not what type the value is, but whether you know the value now and whether this name will ever point somewhere else.
Because a name exists only once something declares it, a missing declaration is not a default value, it is a failed lookup. After let count; the expression count evaluates to undefined, but a count that nobody declared throws a ReferenceError the instant you read it, and an assignment to it either throws or quietly invents a global depending on strict mode. Current practice is to reach for const first, switch to let when the binding genuinely has to be repointed, and leave var for code you are maintaining rather than code you are writing. Choosing that way makes each declaration announce its intent, so a reader can tell at a glance which names in a function are expected to move.
var legacyCount = 3; // old keyword, still valid JavaScript
let attempts = 0; // this one is updated below
const MAX_ATTEMPTS = 5; // value settled at the declaration
let lastError; // declared now, value supplied later
let w = 2, h = 3, area = w * h; // three bindings in one statement
console.log(legacyCount, attempts, MAX_ATTEMPTS);
console.log(lastError);
console.log(area);
attempts = attempts + 1;
lastError = "timeout";
console.log(attempts, lastError, attempts < MAX_ATTEMPTS);A declaration creates a named binding in a scope, and the keyword you pick plus whether you supply an initial value is what decides how that binding may be used.
Worked examples
A const must be given its value on the spot
Shows that let tolerates a missing initializer while const rejects one before the code ever runs.
let ready;
console.log(ready);
try {
eval("const LIMIT;");
} catch (err) {
console.log(err.name);
}
const LIMIT = 5;
console.log(LIMIT);Example explained
Line 1let ready; creates the binding and the engine initializes the slot to undefined, so the next line prints undefined instead of failing.
Line 2const LIMIT; is not valid JavaScript, so it has to be hidden inside a string here; written directly it would stop this whole file from parsing.
Line 3err.name is SyntaxError rather than ReferenceError or TypeError, which tells you the problem was found while reading the code, not while executing it.
Line 4const LIMIT = 5; declares and initializes in one statement, the only form const accepts.
Declared with no value versus never declared
Demonstrates that undefined and an undeclared name are different situations even though typeof reports the same string.
let ready;
console.log(typeof ready);
console.log(typeof neverDeclared);
console.log(ready === undefined);
try {
console.log(neverDeclared);
} catch (err) {
console.log(err.name);
}Example explained
Line 1typeof ready is "undefined" because the binding exists and holds undefined.
Line 2typeof neverDeclared is also "undefined", so typeof on its own cannot tell you whether a name was declared.
Line 3ready === undefined is true, confirming that omitting the initializer stores a real value rather than leaving a hole.
Line 4Reading neverDeclared directly throws ReferenceError: the lookup fails in every enclosing scope and JavaScript reports that instead of inventing a value.
One keyword, several names
Shows the comma-separated declaration list and why chained assignment declares only the first name.
"use strict";
let first = "a", second = "b", both = first + second;
console.log(first, second, both);
try {
let p = q = 5; // only p is declared here
} catch (err) {
console.log(err.name);
}
console.log(typeof q);Example explained
Line 1The comma list creates three bindings from one let, and initializers run left to right, so both can already use first and second.
Line 2In let p = q = 5; the keyword covers only p; everything right of the first equals sign is an ordinary expression, so q = 5 assigns to a name nothing declared.
Line 3Under "use strict" that assignment throws ReferenceError; in a non-strict classic script it would instead have silently created a global named q.
Line 4typeof q prints "undefined" because the throw happened before any binding or global property was made.
Important notes
const locks the binding, not the contents: const list = []; followed by list.push(1) is perfectly legal, and the exact limits get their own lesson.
ES modules and class bodies are always strict, so keyword-less assignment throws there even if the same code appeared to work in an old inline script.
Common mistakes
Writing const total; with the intention of filling it in later. The missing initializer is a parse-time SyntaxError, so nothing in the file runs at all, not just that line, and the earlier console.log calls you were relying on never appear. Use let total; instead.
Assigning without any keyword, as in count = 0;. That is not a declaration: in a module or under "use strict" it throws ReferenceError, and in a plain script it creates a property on the global object that any other script can overwrite.
Writing let a = b = 0; and assuming both names were declared. Only a is declared; b is an undeclared assignment, so the line either throws or leaks a global, and moving that code into a module later turns a silent bug into a crash.
Try it yourself
Change, predict, then run
In a browser console, declare const currency = "USD" and let amount;, log both so you see undefined for one of them, then assign 19.99 to amount and log it again. Now change let amount; to const amount; and read the error the console reports before it executes any of your lines.
Open the JavaScript workspaceCheck your understanding
A function computes a value inside an if/else and needs one name to hold whichever branch ran. Which declaration works, and why?
- let result; because a declaration may omit the initializer, and the branches assign to the binding afterwards
- const result; because const treats the first assignment it sees as the initializer
- result = null; with no keyword, because the first assignment declares the name where it appears
- const result = undefined; because the branches can then replace undefined with the real value
Show answer
let may be declared with no value; the engine initializes the slot to undefined, so either branch can assign the real value later. The last option is tempting because it satisfies const's requirement for an initializer, but that only creates a binding that is already locked, so the branch assignment throws instead of storing anything, and the second option is not even valid syntax. Option three declares nothing at all and throws in any module or strict-mode function.