Back to blog

Let vs Const vs Var in JavaScript: Scope, Hoisting, and Best Practices

Variable declaration is one of the first concepts developers learn in JavaScript. Historically, the var keyword was the only option. However, var introduced quirks (such as scope bleeding and unexpected undefined states) that led to difficult-to-trace bugs.

To address these flaws, ECMAScript 2015 (ES6) introduced the let and const keywords.

In this guide, we will analyze the technical differences between var, let, and const, examine block scope, explain variable hoisting and the Temporal Dead Zone (TDZ), and establish best practices for your code.

1. Scope: Function vs. Block

The core difference between these keywords is how their scope boundaries are defined:

  • var is Function-Scoped: If you declare a variable using var inside a function, it is accessible anywhere inside that function. However, if you declare it inside an if block or for loop, the variable leaks out into the surrounding function or global scope.
if (true) {
  var x = 10;
}
console.log(x); // Outputs 10. The variable leaked outside the block!
  • let and const are Block-Scoped: Variables declared using let or const are strictly bound to the nearest block (enclosed in curly braces {}). They cannot be read outside that block.
if (true) {
  let y = 20;
}
console.log(y); // ReferenceError: y is not defined

2. Hoisting and the Temporal Dead Zone (TDZ)

In JavaScript, variable declarations are lifted (hoisted) to the top of their enclosing scope during compilation. However, how they are initialized differs:

var Hoisting

When var is hoisted, it is initialized with a value of undefined. You can access a var variable before its actual declaration line without triggering an error:

console.log(myVar); // Outputs: undefined
var myVar = 'hello';

let and const Hoisting (The TDZ)

let and const declarations are also hoisted, but they are not initialized.

The period between the beginning of the block scope execution and the line where the variable is declared is called the Temporal Dead Zone (TDZ). If you attempt to access a let or const variable inside the TDZ, the JavaScript engine throws an error instantly:

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

This prevents developers from reading uninitialized states, eliminating silent bug variables.

3. Redeclaration and Reassignment

  • Redeclaration: var allows you to declare the same variable name multiple times in the same scope. let and const throw a SyntaxError if you attempt duplicate declarations.
  • Reassignment: var and let allow you to change their values over time. const stands for "constant" and cannot be reassigned:
const maxLimit = 100;
maxLimit = 200; // TypeError: Assignment to constant variable.

const and Object Mutation

A common point of confusion is how const handles arrays and objects.

Declaring a variable as const prevents reassignment (meaning you cannot point the variable name to a different memory address). However, it does not make the underlying data immutable. You can mutate the keys or items inside a constant object or array:

const user = { name: 'Alex' };

// This is permitted (mutation)
user.name = 'Bob';

// This throws an error (reassignment)
user = { name: 'Charlie' }; 

If you require absolute immutability, you must use Object.freeze(user).

Recommended Style Rules

To maintain code reliability, follow these style rules:

  1. Use const by default: Declaring variables as const signals to other developers that the variable name will not be repointed, preventing accidental side-effect modifications.
  2. Use let only when you expect reassignment: Ideal for loops, counters, mathematical accumulations, or state toggles.
  3. Never use var: var is legacy syntax. It has no benefits in modern ES6+ environments.

Conclusion

Understanding the scope boundaries and hoisting mechanics of JavaScript variable keywords is essential for writing clean code. By eliminating the function-scoped var keyword and relying on the block-scoped safety of const (for immutable references) and let (for reassignable states), you can write clean, predictable JavaScript.