Var, Let, and Const: Mastering JavaScript Variable Declarations

Dive deep into JavaScript's `var`, `let`, and `const` keywords to understand their distinct scoping, hoisting, and immutability behaviors. Learn to write cleaner, more predictable, and robust modern JavaScript.

T

TechScale

October 8, 2026 8 min read
Share:
Var, Let, and Const: Mastering JavaScript Variable Declarations

The Evolution of Variable Declaration in JavaScript

In the world of JavaScript, variable declaration is a fundamental concept that significantly impacts code predictability, maintainability, and architectural soundness. Prior to ES2015 (ES6), var was the sole keyword for declaring variables. While functional, var introduced several quirks related to scoping and hoisting that often led to unexpected behavior and subtle bugs, especially in complex applications.

With the advent of ES6, two new keywords, let and const, were introduced to address these limitations. These modern declarations provide developers with more granular control over variable lifecycle and visibility, aligning JavaScript with best practices found in other robust programming languages.

Understanding the distinct characteristics of var, let, and const is not just about syntax; it's about making deliberate architectural choices that enhance code quality and prevent common pitfalls. This deep-dive will explore the nuances of each keyword, demonstrating their behaviors with practical code examples and guiding you towards optimal usage in your projects.

var: The Legacy Declaration

Introduced in the early days of JavaScript, var declares variables with function scope. This means a variable declared with var is accessible anywhere within the function it's declared in, regardless of block boundaries like if statements or for loops. If declared outside any function, it becomes a global variable.

Key characteristics of var:

  • Function Scope: Variables declared with var are scoped to the nearest function or globally if outside any function.
  • Hoisting: var declarations are "hoisted" to the top of their containing function or global context. This means you can access a var variable before its actual declaration in the code, though its value will be undefined until the assignment is reached.
  • Re-declaration and Re-assignment: Variables declared with var can be easily re-declared within the same scope without error, and their values can be re-assigned at any point. This flexibility, while sometimes convenient, often leads to accidental overwrites and difficult-to-trace bugs in larger codebases.
var-example.js
javascript
function demonstrateVar() {
  console.log(myVar); // undefined (due to hoisting)

  if (true) {
    var myVar = "I am var";
    var myVar = "I am re-declared var"; // No error
    console.log(myVar); // I am re-declared var
  }

  console.log(myVar); // I am re-declared var (function-scoped)

  for (var i = 0; i < 3; i++) {
    setTimeout(function() {
      console.log(`Index (var): ${i}`); // Logs 'Index (var): 3' three times
    }, 100 * i);
  }
  console.log('Loop finished with var i = ' + i); // 3 (i leaks out of loop)
}

demonstrateVar();

The `var` Loop Trap

Be extremely cautious when using `var` inside loops, especially with asynchronous operations like `setTimeout`. Due to `var`'s function scope and the closure behavior, the loop variable (`i` in the example) will have its final value when the `setTimeout` callbacks eventually execute, leading to incorrect results. This is a classic source of bugs.

let: Embracing Block Scope

let was introduced as a more predictable alternative to var, primarily by bringing block scope to JavaScript. A variable declared with let is confined to the block, statement, or expression it was declared in. This means it's not accessible outside the curly braces {} of the block where it's defined.

Key characteristics of let:

  • Block Scope: Variables declared with let are scoped to the nearest enclosing block ({}).
  • Temporal Dead Zone (TDZ): Unlike var, let variables are not hoisted in the same way. While their declarations are processed at the beginning of their scope, they are not initialized. Attempting to access a let variable before its declaration within its scope will result in a ReferenceError – this period is known as the Temporal Dead Zone.
  • No Re-declaration, Allows Re-assignment: You cannot re-declare a let variable within the same scope. However, you can re-assign its value. This prevents accidental variable clobbering and makes code easier to reason about.

let is the preferred choice for variables that need to be re-assigned, such as counters in loops or mutable state within a function.

let-example.js
javascript
function demonstrateLet() {
  // console.log(myLet); // ReferenceError: Cannot access 'myLet' before initialization (TDZ)

  let myLet = "Initial let value";
  console.log(myLet); // Initial let value

  if (true) {
    let myLet = "Block-scoped let"; // This is a new variable, not a re-declaration error
    console.log(myLet); // Block-scoped let
    // let myLet = "Another let"; // SyntaxError: 'myLet' has already been declared
  }

  console.log(myLet); // Initial let value (outer scope untouched)

  for (let i = 0; i < 3; i++) {
    setTimeout(function() {
      console.log(`Index (let): ${i}`); // Logs Index (let): 0, 1, 2
    }, 100 * i);
  }
  // console.log('Loop finished with let i = ' + i); // ReferenceError: i is not defined
}

demonstrateLet();

Understanding the Temporal Dead Zone (TDZ)

The TDZ for `let` and `const` variables exists from the start of their enclosing scope until their actual declaration. This means even though the browser 'knows' about the variable, you cannot access it before its line of declaration. This helps catch potential bugs where a variable might be used before it's properly defined.

const: Immutable References, Not Immutable Values

const also arrived with ES6 and shares many characteristics with let, particularly block scope and the Temporal Dead Zone. The key distinction of const is its commitment to a constant reference. Once a variable is declared with const, its value cannot be re-assigned, nor can it be re-declared.

Key characteristics of const:

  • Block Scope: Like let, const variables are scoped to the nearest enclosing block ({}).
  • Temporal Dead Zone (TDZ): const variables also reside in a TDZ, meaning they cannot be accessed before their declaration within their scope.
  • No Re-declaration, No Re-assignment: The most significant feature: a const variable must be initialized at the time of declaration, and its value cannot be changed afterwards. Attempting to re-assign or re-declare will result in a TypeError or SyntaxError, respectively.
  • Constant Reference, Not Constant Value: It's crucial to understand that const ensures the reference remains constant, not necessarily the value it points to. For primitive values (numbers, strings, booleans), this means the value itself is immutable. However, for objects and arrays, const only prevents re-assigning the variable to a new object or array. The properties or elements of the original object/array can still be modified.

const is the recommended default for variable declarations in modern JavaScript, promoting immutability and making code more predictable and easier to debug.

const-example.js
javascript
function demonstrateConst() {
  // console.log(myConst); // ReferenceError: Cannot access 'myConst' before initialization (TDZ)

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

  if (true) {
    const PI = "Not PI"; // Block-scoped, new variable, no error
    console.log(PI); // Not PI
  }

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

  user.age = 31; // Allowed: object property can be modified
  console.log(user); // { name: 'Alice', age: 31 }

  // user = { name: "Bob" }; // TypeError: Assignment to constant variable.
}

demonstrateConst();

`const` Does Not Mean Immutable Object

A common misconception is that `const` makes an object or array immutable. This is incorrect. `const` prevents re-assignment of the variable itself, but the *contents* of the object or array it points to can still be modified. For true immutability, consider using methods like `Object.freeze()` or libraries like Immer or Immutable.js.

Synthesizing the Differences: When to Use Which

Here's a concise comparison of var, let, and const:

  • Scope:
    • var: Function-scoped or global.
    • let: Block-scoped.
    • const: Block-scoped.
  • Hoisting:
    • var: Hoisted to the top of its function/global scope, initialized with undefined.
    • let/const: Hoisted to the top of their block scope but uninitialized, leading to a Temporal Dead Zone (TDZ) before declaration.
  • Re-declaration:
    • var: Allowed within the same scope.
    • let: Not allowed within the same scope.
    • const: Not allowed within the same scope.
  • Re-assignment:
    • var: Allowed.
    • let: Allowed.
    • const: Not allowed (must be initialized at declaration).

Modern JavaScript Best Practices

For most modern JavaScript development, the general guidance is clear:

  1. Use const by Default: This encourages writing functional, predictable code by minimizing mutable state. Declare variables with const unless you have a specific reason to re-assign them.
  2. Use let When Re-assignment is Necessary: If a variable's value needs to change during its lifecycle (e.g., loop counters, variables representing mutable state), let is the appropriate choice. Its block scope prevents the issues associated with var.
  3. Avoid var: In new codebases, there is rarely a good reason to use var. Its function-scoping and permissive re-declaration behavior can lead to hard-to-debug issues. Legacy codebases might still contain var, but for new development, let and const offer superior control and clarity.

Conclusion: Writing Robust and Maintainable JavaScript

The introduction of let and const fundamentally changed how developers declare and manage variables in JavaScript, moving the language towards more predictable and robust patterns. By understanding the distinct scoping, hoisting, and re-assignment rules of var, let, and const, you gain powerful tools to prevent common bugs, improve code readability, and enforce clearer architectural boundaries within your applications.

Embrace const as your default, opt for let when mutability is genuinely required, and strategically phase out var from your new projects. This thoughtful approach to variable declaration is a hallmark of a skilled JavaScript engineer and contributes significantly to the overall health and maintainability of any codebase.

Enjoyed this deep dive?

Share:
Tags:JavaScriptES6VariablesScopeHoistingBest Practices

Discussion & Feedback

0 thoughts shared by the community

💬

Loading discussion thread...