JavaScript var vs let vs const: Complete Guide with Examples

Last updated: Apr 12, 2025

Introduction: Declaring Variables in JavaScript

Variables are fundamental building blocks in any programming language, acting as named containers for storing
data values. In JavaScript, how you declare a variable significantly impacts its behavior, especially
concerning its scope (where it’s accessible) and whether its value can be changed. Before ES6 (ECMAScript
2015), var was the only way to declare variables. ES6 introduced let and const, providing more control
and helping developers write cleaner, less error-prone code.

Understanding var (The Old Way)

The var keyword was the original way to declare variables in JavaScript. It has some quirks that can lead
to unexpected behavior in modern development.

Scope

  • Scope:var variables are eitherglobally scoped(if declared outside
    any function) orfunction scoped(if declared inside a function). They arenotblock-scoped (e.g., inside if statements or for loops).

  • Hoisting:var declarations are “hoisted” to the top of their scope (global or function)
    during compilation. This means the variable exists throughout the scope, but it’s initialized with
    undefined until its declaration line is reached.

  • Re-declaration:You can re-declare the same variable using var within the same scope
    without errors.

  • Re-assignment:You can update the value of a var variable.

  • Global Object Property:When declared in the global scope (outside functions), var
    creates a property on the global object (window in browsers, global in Node.js).However, this does not apply to variables declared in modules (files using import/export).

  • Global Object Property Exception:Note that var declarations in ES modules (files using
    import/export)
    do not create properties on the global object, even at the top level.

Example (var Scope):

function varScopeTest() {
  if (true) {
    var message = "Hello from inside if"; 
  }
  console.log(message); // Output: "Hello from inside if" - message leaks outside the block
}
varScopeTest();
// console.log(message); // ReferenceError: message is not defined (outside the function scope)

Hoisting

Example (var Hoisting):

console.log(myVar); // Output: undefined (hoisted but not yet assigned)
var myVar = 10;
console.log(myVar); // Output: 10

Re-declaration

Example (var Re-declaration):

var x = 5;
console.log(x); // Output: 5
var x = 20; // No error
console.log(x); // Output: 20

Due to these behaviors, especially the lack of block scope, var is generally avoided in modern JavaScript
development in favor of let and const.

Introducing let (The Modern Mutable Choice)

Introduced in ES6, let provides a more predictable way to declare variables whose values might need to
change later.

Block Scope

  • Scope:let variables areblock-scoped. They only exist within the
    block ({...}) in which they are declared (e.g., inside an if, for loop, or just a standalone block).

  • Hoisting & Temporal Dead Zone (TDZ):While let and const declarations are hoisted,
    their TDZ prevents any access before declaration. Attempting to access the variable before its declaration
    will result in a ReferenceError. This differs from var which returns undefined when accessed before
    declaration.

  • Re-declaration:You cannot re-declare the same variable using let within the same
    scope.

  • Re-assignment:You can update (reassign) the value of a let variable.

  • Global Object Property:let does not create a property on the global object when
    declared in the global scope.

Example (let Scope):

function letScopeTest() {
  if (true) {
    let blockMessage = "Hello from inside if"; 
    console.log(blockMessage); // Output: "Hello from inside if"
  }
  // console.log(blockMessage); // ReferenceError: blockMessage is not defined (outside the block scope)
}
letScopeTest();

Temporal Dead Zone (TDZ)

Example (let TDZ):

// console.log(myLet); // ReferenceError: Cannot access 'myLet' before initialization (TDZ)
let myLet = 30;
console.log(myLet); // Output: 30

Re-declaration Rules

Example (let Re-declaration):

let y = 15;
console.log(y); // Output: 15
// let y = 25; // SyntaxError: Identifier 'y' has already been declared

Use let when you need a variable whose value will change over time, like loop counters or temporary
assignments.

Understanding const (For Constant References)

const (also introduced in ES6) is used to declare variables whose values are intended to remain constant.
However, there’s an important nuance regarding objects and arrays.

Working with Objects

  • Scope:const variables areblock-scoped, just like let.

  • Hoisting & Temporal Dead Zone (TDZ):const declarations are hoisted but not
    initialized, creating a TDZ, just like let. Accessing before declaration results in a ReferenceError.

  • Re-declaration:You cannot re-declare the same variable using const within the same
    scope, just like let.

  • Re-assignment:Youcannotreassign a const variable after it’s
    declared. This is its primary characteristic.

  • Initialization:const variablesmustbe initialized with a value when
    they are declared.

  • Global Object Property:const does not create a property on the global object.

  • Important Nuance (Objects/Arrays):const makes thebinding(the variable name)
    constant, not necessarily thevalueit holds. If a const variable holds an object or an array,
    you cannot reassign the variable to a new object or array, but you can modify the properties of the
    object or the elements of the array.

  • Object Immutability: If you need true immutability for an object declared with const,
    you can use
    Object.freeze():
    ```javascript
    const frozenPerson = Object.freeze({ name: “Alice” });
    frozenPerson.name = “Bob”; // This will fail silently in non-strict mode
    // or throw TypeError in strict mode

    
    
  • Using const with Destructuring: When using destructuring assignment with const, each
    destructured variable becomes a constant:
    ### Destructuring

    const { name, age } = person;
    name = "Bob"; // TypeError: Assignment to constant variable
    // However, if person is an object:
    person.name = "Bob"; // This works as we're modifying the object, not reassigning the const
  • Object Properties: While Object.freeze() can prevent modifications to an object’s
    direct properties,
    it only creates a shallow freeze. Nested objects can still be modified unless they are also frozen:
    ### Object.freeze()

    const person = Object.freeze({
      name: "Alice",
      address: { city: "London" }
    });
    person.name = "Bob"; // Fails (frozen)
    person.address.city = "Paris"; // Works (nested object not frozen)
    

Example (const Re-assignment):

const PI = 3.14159;
console.log(PI); // Output: 3.14159
// PI = 3.14; // TypeError: Assignment to constant variable.

Example (const Initialization):

// const GREETING; // SyntaxError: Missing initializer in const declaration
const GREETING = "Hello"; 

Example (const with Objects):

const person = { name: "Alice", age: 30 };
console.log(person); // Output: { name: 'Alice', age: 30 }

// This is allowed: Modifying the object's property
person.age = 31; 
console.log(person); // Output: { name: 'Alice', age: 31 }

// This is NOT allowed: Reassigning the constant variable
// person = { name: "Bob", age: 40 }; // TypeError: Assignment to constant variable.

Use const by default for all declarations. This signals that the variable’s assignment shouldn’t change,
which makes code easier to reason about and prevents accidental reassignments.

Additional Scoping Considerations

When working with ES modules, there’s an additional scope to consider:

  • Module Scope:Variables declared at the top level of a module (file) are scoped to that
    module, regardless of whether you use var, let, or const.

  • Import/Export:You can only export variables that are declared at the module scope level.

// moduleA.js
export const config = { api: 'example.com' };
var helper = 'utility'; // Only accessible within this module
export { helper }; // Can still export it explicitly

// moduleB.js
import { config, helper } from './moduleA.js';
console.log(config.api); // Works
console.log(window.helper); // undefined - not added to global object

Key Differences Summarized

Feature
var
let
const

Scope
Function or Global
Block
Block

Hoisting
Hoisted & Initialized (undefined)
Hoisted & Not Initialized (TDZ)
Hoisted & Not Initialized (TDZ)

Re-declaration (same scope)
Allowed
Not Allowed
Not Allowed

Re-assignment
Allowed
Allowed
Not Allowed

Must Be Initialized
No
No
Yes

Global Object Property
Yes (in global scope)
No
No

When to Use Which?

The modern best practice in JavaScript (ES6+) is straightforward:

Use const by default: Start by declaring all your variables with const. This makes
your code more predictable as it guarantees the variable binding will not be reassigned.
Use let only if you need to reassign the variable: If you know a variable’s value needs
to change during its lifecycle (e.g., a loop counter, a state variable that gets updated), then use let.

Avoid var: There is generally no reason to use var in modern JavaScript codebases.
let and const offer better scoping rules and help prevent common bugs associated with var.
Consider block scope for temporary variables: Use blocks (even without control
statements) to limit
the scope of temporary variables:
{
const temp = heavyComputation();
// temp only exists in this block
doSomethingWith(temp);
}

By following these guidelines, you can write cleaner, more maintainable, and less error-prone JavaScript
code.