Key Takeaways
- Use
constby default andletwhen you need to reassign a binding—the association between a name and a value. constprevents reassignment, not changes to an object's properties or an array's elements.letandconstrespect block boundaries. Inside a function,vardoes not stay inside an ordinary nested block.- Reading a
letorconstbinding before initialization throwsReferenceError; top-level scope also depends on whether you're running a script or a module.
For JavaScript variable declarations, choose const unless you'll assign a different value to the same name; use let when reassignment is necessary. Generally avoid var in new code because its broader scope and initialization behavior make accidental reuse easier.
ES6 is the historical name for ECMAScript 2015, the sixth edition. These declaration rules remain foundations of modern JavaScript, not techniques limited to that release.
Declaring a Variable
Start with a declaration that gives a name an initial value:
let x = 2;Here, x is the identifier, or variable name, and = 2 is the initializer. Initialization gives the binding its first value; reassignment replaces that value later.
Choose names that explain what a value represents:
let firstName = "Alice";
console.log(firstName); // AliceThis guide focuses on var, let, and const. Function declarations, class declarations, and imports also introduce bindings, with their own rules.
Identifier Rules and Descriptive Names
For everyday names, use letters, digits, underscores, and dollar signs, but don't start with a digit. That familiar ASCII convention isn't the complete language rule: identifiers also support many Unicode characters and Unicode escapes.
let number2;
const café = "open";
console.log(number2); // undefined
console.log(café); // openThe following is a non-runnable syntax illustration, not an example to paste into a working program:
let 2number; // SyntaxError: an identifier cannot start with a digit- Names are case-sensitive:
firstNameandfirstnameare different identifiers. - Reserved words such as
varandfunctioncannot be variable names. - Some restrictions depend on context:
awaitis reserved in modules and async function bodies, while strict mode adds other restrictions.
Use descriptive names such as firstName or itemCount, and consult the identifier and reserved-word reference for the full rules.
JavaScript let vs const vs var
| Behavior | var | let | const |
|---|---|---|---|
| Limited to an ordinary nested block | No | Yes | Yes |
| Allows reassignment | Yes | Yes | No |
| Allows repeated declarations with the same keyword in one scope | Yes | No | No |
| Initial state on entering scope | Initialized to undefined | Uninitialized | Uninitialized |
| Standalone declaration requires an initializer | No | No | Yes |
The block row refers to ordinary blocks, such as an if body. Top-level script and module rules are covered below. For another focused comparison, see JavaScript var, let, and const.
var: Recognize Function Scope in Existing Code
Inside a function, var belongs to the function rather than an ordinary nested block. Repeating a var declaration can therefore reuse the same binding instead of creating a separate one.
function compareScope() {
var count = 0;
if (true) {
var count = 1; // Same function-scoped binding
let blockValue = 2;
const fixedValue = 3;
console.log(count, blockValue, fixedValue); // 1 2 3
}
console.log(count); // 1
try {
console.log(blockValue);
} catch (error) {
console.log(error.name); // ReferenceError
}
try {
console.log(fixedValue);
} catch (error) {
console.log(error.name); // ReferenceError
}
}
compareScope();The inner var count overwrites the existing binding's value. The other two names aren't accessible after the block. This is why an if boundary alone doesn't protect a var variable. Review JavaScript control flow if those block boundaries are unfamiliar.
let: Allow Reassignment, Not Same-Scope Redeclaration
A let declaration can omit its initializer. Once that declaration executes, the new binding holds undefined until you assign another value.
let number2;
console.log(number2); // undefined
number2 = 2;
number2 = 3;
console.log(number2); // 3Reassignment isn't redeclaration. You cannot declare the same name twice with let in one scope; that produces a SyntaxError. Mixing let with another declaration of the same name in that scope is also prohibited.
Non-runnable syntax illustration:
let total = 4;
let total = 5; // SyntaxError: duplicate declaration in the same scopeconst: Prevent Reassignment, Not Mutation
A standalone const declaration needs an initializer. After initialization, assigning another value to that binding throws TypeError.
const x = 4;
let y = 4;
y = 5;
console.log(y); // 5
try {
x = 5;
} catch (error) {
console.log(error.name); // TypeError
}
console.log(x); // 4For objects and arrays, distinguish reassignment from mutation. Reassignment replaces the value associated with a name; mutation changes the existing object's contents. const only prevents the first.
const user = { name: "Alice" };
user.name = "Bob"; // Allowed: mutate the existing object
const names = ["Alice"];
names[0] = "Bob"; // Allowed: change an array element
console.log(user.name, names[0]); // Bob Bob
try {
user = { name: "Carol" }; // Not allowed: replace the binding's value
} catch (error) {
console.log(error.name); // TypeError
}For property access and updates, continue with JavaScript objects. If you need to restrict object changes, Object.freeze() is a separate mechanism, but it is shallow: nested objects aren't automatically frozen.
Understanding JavaScript Variable Scope
Scope determines where a binding can be referenced by name. JavaScript uses lexical scope: the nesting of your source code determines which surrounding bindings a function can access.
Outer Scope: A Function Can Read Surrounding Bindings
let x = 2;
function add() {
return x + 2;
}
console.log(add()); // 4add() reads x from its outer scope. That doesn't prove x is global: the surrounding scope could be a module or another function. See JavaScript functions for more on function definitions and calls.
Function Scope: Local Bindings Aren't Visible Outside
function add() {
let x = 2;
return x + 2;
}
console.log(add()); // 4
try {
console.log(x);
} catch (error) {
console.log(error.name); // ReferenceError
}Here, x belongs to the function body. With no separate outer x, reading it outside the function throws ReferenceError; it does not return undefined.
Block Scope: An Inner Declaration Can Shadow an Outer One
function add() {
let x = 2;
{
let x = 4;
console.log(x); // 4: inner binding
}
return x + 2;
}
console.log(add()); // 4: outer x is still 2The nested block creates a different x. This is shadowing: the inner name hides the outer name within the inner scope without replacing its binding.
File's outer scope
+-- add() function scope: x = 2
+-- nested block: x = 4 (shadows the function's x)
+-- after nested block: x = 2
Outside add(): the function's local x is not visibleGlobal Scope Depends on Scripts and Modules
“Top-level” means outside nested functions and blocks. It doesn't always mean global:
- Browser classic script: top-level
varcreates a property on the global object. Top-levelletandconstcreate global lexical bindings, not properties ofglobalThis. - ES module: top-level
var,let, andconstare scoped to the module rather than becoming global declarations. - Node.js CommonJS file: top-level declarations remain local to the module because Node.js wraps the file in a function.
Don't use the absence of a globalThis property as proof that no binding with that name exists. Global lexical bindings and global-object properties are different things.
Hoisting and the Temporal Dead Zone
Hoisting describes declaration behavior that takes effect before execution reaches the declaration's written position. A var binding starts initialized to undefined, but its initializer runs where you wrote it.
let and const bindings exist from the start of their scope but remain uninitialized until their declarations initialize them. That interval is the temporal dead zone (TDZ), and accessing the binding during it throws ReferenceError.
{
console.log(beforeVar); // undefined
var beforeVar = 2;
try {
console.log(beforeLet);
} catch (error) {
console.log(error.name); // ReferenceError
}
let beforeLet = 2;
console.log(beforeLet); // 2
}“let isn't hoisted” is therefore incomplete shorthand. The important distinction is whether the binding has been initialized and can be accessed—not whether the engine knows the name exists.
Common Mistakes
- Choosing let because an object changes: choose based on whether you replace the binding's value, not whether you update properties.
- Replacing var mechanically: check for reads before declarations, repeated declarations, and uses outside nested blocks before changing existing code.
- Confusing shadowing with reassignment: declaring an inner
let xcreates another binding; writingx = 4without a declaration updates the binding that name resolves to.
Keep the decision simple: start with const, switch to let when reassignment is part of the logic, and understand var when maintaining existing code.
FAQ
What is the difference between let, const, and var?
let and const are block-scoped; inside a function, var ignores ordinary nested block boundaries. Use const when you won't reassign the binding and let when you will.
Does const make an object immutable?
No. const prevents reassignment of the binding, but object properties and array elements can still change. Attempting to reassign the binding throws TypeError.
What happens if I declare a variable without initializing it?
A new let or var binding declared without an initializer has the value undefined once the declaration is reached. A standalone const declaration without an initializer is a SyntaxError.
What is the temporal dead zone in JavaScript?
The temporal dead zone is the interval from entering a binding's scope until that binding is initialized. Accessing a let or const binding during that interval throws ReferenceError.
Are top-level let and const variables global?
In a browser classic script, top-level let and const create global lexical bindings, not properties of globalThis. In an ES module they are module-scoped; Node.js CommonJS files keep them local to the module.
Further Reading
- MDN: let — block scope, redeclarations, and initialization.
- MDN: const — constant bindings and mutable objects.
- MDN: var — scope, hoisting, and repeated declarations.
- MDN: Hoisting — declaration behavior and the TDZ.
- MDN: Lexical grammar — identifiers, Unicode, and reserved words.
- MDN: JavaScript modules — module scope and imports.
- Node.js: CommonJS modules — the module wrapper and file-local variables.
- MDN: Object.freeze() — shallow freezing and its limits.
