Tutorials

JavaScript ES6 Variables

Key Takeaways

  • Use const by default and let when you need to reassign a binding—the association between a name and a value.
  • const prevents reassignment, not changes to an object's properties or an array's elements.
  • let and const respect block boundaries. Inside a function, var does not stay inside an ordinary nested block.
  • Reading a let or const binding before initialization throws ReferenceError; 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); // Alice

This 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é); // open

The 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: firstName and firstname are different identifiers.
  • Reserved words such as var and function cannot be variable names.
  • Some restrictions depend on context: await is 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

Behaviorvarletconst
Limited to an ordinary nested blockNoYesYes
Allows reassignmentYesYesNo
Allows repeated declarations with the same keyword in one scopeYesNoNo
Initial state on entering scopeInitialized to undefinedUninitializedUninitialized
Standalone declaration requires an initializerNoNoYes

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); // 3

Reassignment 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 scope

const: 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); // 4

For 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()); // 4

add() 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 2

The 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 visible

Global 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 var creates a property on the global object. Top-level let and const create global lexical bindings, not properties of globalThis.
  • ES module: top-level var, let, and const are 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 x creates another binding; writing x = 4 without 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

Written by Sam Erickson

Software engineer. I write every article on StackChief myself. Author of Mastering the Tech Interview. More about me