Skip to main content

How to Declare and Use Variables in JavaScript: let, const, and var

Variables are the foundation of every program. They store data, give it a name, and let you work with it throughout your code. Whether you are tracking a user's score, holding an API response, or counting loop iterations, you are using variables.

JavaScript offers three ways to declare variables: let, const, and var. Each behaves differently in subtle but important ways, and choosing the right one matters. This guide explains what variables are, how each declaration keyword works, how JavaScript handles variables behind the scenes through hoisting and the temporal dead zone, and the most common mistakes developers make when working with them.

What Is a Variable?

A variable is a named container for a piece of data. When you create a variable, you are telling JavaScript: "Reserve a spot in memory, give it this name, and store this value in it."

The Box Analogy

The most common way to explain variables is the "labeled box" analogy:

let userName = "Alice";

Imagine a box with the label userName written on it. Inside the box is the value "Alice". You can look inside the box (read the value), replace its contents (reassign), or use the label to refer to whatever is inside.

let userName = "Alice";       // Put "Alice" in the box
console.log(userName); // Look inside: "Alice"

userName = "Bob"; // Replace the contents
console.log(userName); // Look inside: "Bob"

Beyond the Box: The Label Analogy

The box analogy breaks down with objects and arrays. A more accurate way to think about variables is as labels (or sticky notes) attached to values in memory.

let user = { name: "Alice" };
let admin = user; // Both labels point to the SAME object in memory

Here, user and admin are two different labels stuck to the same object. Changing the object through one label affects what you see through the other:

admin.name = "Bob";
console.log(user.name); // "Bob" (same object, same data)

This distinction between the "box" model (for primitives) and the "label" model (for objects) becomes critical as you progress. We will revisit this topic in depth when covering objects and references.

let: The Modern Way to Declare Variables

let was introduced in ES6 (2015) and is the standard way to declare variables in modern JavaScript.

Basic Usage

let age = 25;
console.log(age); // 25

age = 26; // Reassignment is allowed
console.log(age); // 26

You can declare a variable without assigning a value. It will be undefined until you assign something:

let score;
console.log(score); // undefined

score = 100;
console.log(score); // 100

Block Scope

The defining characteristic of let is block scope. A variable declared with let exists only inside the { } block where it was declared:

if (true) {
let message = "Hello";
console.log(message); // "Hello" (accessible inside the block)
}

console.log(message); // ReferenceError: message is not defined

This applies to all block structures, including if, for, while, and even standalone { } blocks:

for (let i = 0; i < 3; i++) {
console.log(i); // 0, 1, 2
}
console.log(i); // ReferenceError: i is not defined

// Each iteration gets its own 'i' (this matters for closures)

No Redeclaration

You cannot declare the same variable twice with let in the same scope:

let color = "red";
let color = "blue"; // SyntaxError: Identifier 'color' has already been declared

You can declare the same name in a different scope (this is called shadowing, covered later):

let color = "red";

if (true) {
let color = "blue"; // Different scope: this is a NEW variable
console.log(color); // "blue"
}

console.log(color); // "red" (the outer variable is unchanged)

const: Constants and Immutability Misconceptions

const was also introduced in ES6. It declares a variable that cannot be reassigned after its initial value is set.

Basic Usage

const PI = 3.14159;
console.log(PI); // 3.14159

PI = 3.14; // TypeError: Assignment to constant variable.

Unlike let, a const variable must be initialized at the time of declaration:

const name;  // SyntaxError: Missing initializer in const declaration

Block Scope (Same as let)

const has the same block scoping behavior as let:

if (true) {
const secret = "hidden";
console.log(secret); // "hidden"
}

console.log(secret); // ReferenceError: secret is not defined

The Critical Misconception: const Does Not Mean Immutable

This is one of the most common misunderstandings in JavaScript. const prevents reassignment of the variable, not modification of the value.

For primitive values (strings, numbers, booleans), this distinction does not matter because primitives are immutable by nature:

const name = "Alice";
name = "Bob"; // TypeError: cannot reassign
// This is fine because the only way to change a string variable is reassignment

For objects and arrays, const only prevents you from pointing the variable to a different object. The contents of the object can be freely modified:

const user = { name: "Alice", age: 30 };

// Modifying properties is ALLOWED
user.name = "Bob";
user.age = 31;
user.email = "bob@example.com";
console.log(user); // { name: "Bob", age: 31, email: "bob@example.com" }

// Reassigning the entire variable is FORBIDDEN
user = { name: "Charlie" }; // TypeError: Assignment to constant variable.

The same applies to arrays:

const colors = ["red", "green"];

// Modifying the array is ALLOWED
colors.push("blue");
colors[0] = "yellow";
console.log(colors); // ["yellow", "green", "blue"]

// Reassigning is FORBIDDEN
colors = ["purple"]; // TypeError: Assignment to constant variable.
info

Think of const as a constant binding, not a constant value. The label is permanently attached, but if the label points to an object, you can still change what is inside that object. If you need a truly immutable object, use Object.freeze():

const config = Object.freeze({
apiUrl: "https://api.example.com",
timeout: 5000
});

config.timeout = 10000; // Silently fails (or TypeError in strict mode)
console.log(config.timeout); // 5000 (unchanged)

Note that Object.freeze() is shallow. Nested objects are not frozen.

When to Use const

Use const as your default choice. Only switch to let when you know the variable will need to be reassigned:

// Use const for values that won't be reassigned
const MAX_RETRIES = 3;
const apiUrl = "https://api.example.com";
const users = ["Alice", "Bob"]; // Array contents may change, but the variable won't
const config = { debug: false }; // Object properties may change, but the variable won't

// Use let for values that will be reassigned
let count = 0;
count++; // Needs reassignment

let currentUser = null;
currentUser = getLoggedInUser(); // Needs reassignment

let total = 0;
for (let item of cart) {
total += item.price; // Needs reassignment in each iteration
}

var: The Legacy Keyword

var is the original way to declare variables in JavaScript, used from 1995 until let and const were introduced in 2015. You will encounter var in older codebases, tutorials, and Stack Overflow answers, so understanding it is important even though you should not use it in new code.

Basic Usage

var name = "Alice";
console.log(name); // "Alice"

name = "Bob"; // Reassignment allowed
console.log(name); // "Bob"

Function Scope (Not Block Scope)

The most important difference: var is function-scoped, not block-scoped. A var declared inside an if block or a for loop is accessible outside that block:

if (true) {
var message = "Hello";
}
console.log(message); // "Hello" (var leaks out of the if block!)

for (var i = 0; i < 3; i++) {
// loop body
}
console.log(i); // 3 (var leaks out of the for loop!)

Compare this with let:

if (true) {
let message = "Hello";
}
console.log(message); // ReferenceError: message is not defined

for (let i = 0; i < 3; i++) {
// loop body
}
console.log(i); // ReferenceError: i is not defined

var is only contained by functions, not by blocks:

function example() {
var localVar = "I'm local";
console.log(localVar); // "I'm local"
}

example();
console.log(localVar); // ReferenceError (var IS contained by functions)

Redeclaration Is Allowed

Unlike let, var allows you to declare the same variable multiple times without error:

var color = "red";
var color = "blue"; // No error!
var color = "green"; // Still no error!
console.log(color); // "green"

This makes typos harder to catch. If you accidentally redeclare a variable, you overwrite it silently.

var Is Hoisted (With Initialization Remaining in Place)

var declarations are moved to the top of their scope during compilation, but their assignments stay in place:

console.log(greeting);  // undefined (not an error!)
var greeting = "Hello";
console.log(greeting); // "Hello"

The engine processes this as:

var greeting;               // Declaration hoisted to the top
console.log(greeting); // undefined
greeting = "Hello"; // Assignment stays here
console.log(greeting); // "Hello"

This is covered in more detail in the Hoisting section below.

Why You Should Not Use var in New Code

Issuevarlet/const
Scope leakingLeaks out of blocksStays in blocks
RedeclarationAllowed (hides bugs)Forbidden (catches bugs)
Hoisting behaviorHoisted as undefinedHoisted but not accessible (TDZ)
Global object pollutionvar x at top level creates window.xDoes not affect window

let vs. const vs. var: Complete Comparison Table

Featurevarletconst
IntroducedES1 (1997)ES6 (2015)ES6 (2015)
ScopeFunctionBlockBlock
HoistingYes (initialized as undefined)Yes (but in TDZ)Yes (but in TDZ)
Redeclaration in same scopeAllowedForbiddenForbidden
ReassignmentAllowedAllowedForbidden
Must be initializedNoNoYes
Creates window property (global scope)YesNoNo

The Decision Flowchart

Do I need to reassign this variable?
├── No → use const (default choice)
└── Yes → use let

Should I use var?
└── No. Use let or const instead.
tip

The modern best practice:

  1. Use const by default
  2. Use let only when reassignment is needed
  3. Never use var in new code

Variable Naming Rules and Best Practices

What JavaScript Allows

Variable names must follow these rules:

  • Must start with a letter (a-z, A-Z), underscore (_), or dollar sign ($)
  • Can contain letters, digits (0-9), underscores, and dollar signs
  • Cannot be a reserved word (let, const, class, return, etc.)
  • Are case-sensitive (age, Age, and AGE are three different variables)
// Valid names
let userName = "Alice";
let _count = 0;
let $price = 9.99;
let camelCase123 = true;

// Invalid names
let 2fast = "nope"; // Cannot start with a digit
let my-name = "nope"; // Hyphens not allowed
let my name = "nope"; // Spaces not allowed
let class = "nope"; // Reserved word

Naming Conventions

camelCase for regular variables and functions:

let firstName = "Alice";
let itemCount = 42;
let isVisible = true;
let userProfileData = {};

function calculateTotal() { }
function getUserById(id) { }

UPPER_SNAKE_CASE for hard-coded constants known before runtime:

const MAX_LOGIN_ATTEMPTS = 5;
const API_BASE_URL = "https://api.example.com";
const MILLISECONDS_PER_DAY = 86_400_000;

PascalCase for classes and constructors:

class ShoppingCart { }
class UserAccount { }

Writing Semantic Names

The name of a variable should describe what it holds. The name of a function should describe what it does.

// BAD: cryptic, single-letter, or generic names
let d = new Date();
let temp = users.filter(u => u.a);
let data = fetch("/api");
let flag = true;

// GOOD: descriptive, self-documenting names
let currentDate = new Date();
let activeUsers = users.filter(user => user.isActive);
let apiResponse = fetch("/api");
let isAuthenticated = true;

Boolean variables should read like yes/no questions:

let isLoggedIn = true;
let hasPermission = false;
let canEdit = true;
let shouldRedirect = false;
let wasProcessed = true;

Avoid abbreviations unless they are universally understood:

// BAD
let btn = document.querySelector("button");
let usr = getCurrentUser();
let idx = items.findIndex(i => i.id === targetId);

// GOOD
let button = document.querySelector("button");
let currentUser = getCurrentUser();
let targetIndex = items.findIndex(item => item.id === targetId);

// ACCEPTABLE universal abbreviations
let url = "https://example.com"; // Everyone knows URL
let id = 42; // Everyone knows ID
let i = 0; // In short loops, 'i' is standard

Hoisting Explained Simply

Hoisting is JavaScript's behavior of processing all declarations before executing any code. It is as if declarations are "moved" to the top of their scope. Understanding hoisting explains why certain code works (or breaks) in unexpected ways.

How var Is Hoisted

When JavaScript encounters var declarations, it processes them during a compilation phase before any code executes. The declaration is hoisted, but the assignment stays in place:

console.log(name);   // undefined (not an error!)
var name = "Alice";
console.log(name); // "Alice"

JavaScript sees this as:

var name;            // Step 1: Declaration hoisted
console.log(name); // Step 2: undefined (declared but not assigned)
name = "Alice"; // Step 3: Assignment happens here
console.log(name); // Step 4: "Alice"

How let and const Are Hoisted

let and const are also hoisted, but they behave differently. The engine knows they exist from the start of the block, but accessing them before the declaration line throws an error:

console.log(name);  // ReferenceError: Cannot access 'name' before initialization
let name = "Alice";

This is not the same as the variable not existing at all:

// If 'name' were truly not hoisted, you'd get a different error:
console.log(nonExistent); // ReferenceError: nonExistent is not defined
// Note: "not defined" vs. "before initialization"

The error messages are different. "Cannot access before initialization" means the variable is hoisted and the engine knows about it, but it is in the Temporal Dead Zone.

How Functions Are Hoisted

Function declarations are fully hoisted, meaning you can call them before they appear in the code:

greet("Alice");  // "Hello, Alice!" (works before the declaration)

function greet(name) {
console.log(`Hello, ${name}!`);
}

Function expressions (assigned to variables) follow the hoisting rules of their variable keyword:

greet("Alice");  // TypeError: greet is not a function

var greet = function(name) {
console.log(`Hello, ${name}!`);
};

With var, the variable greet is hoisted as undefined, so calling undefined("Alice") throws a TypeError. With let or const, you would get a ReferenceError from the TDZ.

The Temporal Dead Zone (TDZ)

The Temporal Dead Zone is the period between entering a scope where a let or const variable is declared and the line where the declaration actually appears. During this zone, the variable exists but cannot be accessed.

Visualizing the TDZ

{
// --- TDZ for 'greeting' starts here ---

console.log(greeting); // ReferenceError: Cannot access 'greeting' before initialization

let greeting = "Hello"; // --- TDZ for 'greeting' ends here ---

console.log(greeting); // "Hello" (safe to use now)
}

TDZ in Practical Scenarios

In loops:

for (let i = 0; i < 3; i++) {
// Each iteration creates a new scope
// 'i' is available here (its TDZ ended at the loop declaration)
console.log(i);
}

In conditionals:

let x = 10;

if (true) {
// TDZ for the inner 'x' starts here, SHADOWING the outer 'x'
console.log(x); // ReferenceError! Not "10"!
let x = 20; // TDZ ends here
}

This is surprising because you might expect console.log(x) to use the outer x. But because let x = 20 exists in the same block, the inner x shadows the outer one, and the TDZ applies from the start of the block.

In function parameters:

// The TDZ also applies in default parameter expressions
function example(a = b, b = 1) {
console.log(a, b);
}
example(); // ReferenceError: Cannot access 'b' before initialization
// Parameter 'a' tries to use 'b', but 'b' hasn't been initialized yet

Why the TDZ Exists

The TDZ exists to catch bugs. If you try to use a variable before declaring it, you almost certainly have a mistake in your code. With var, this mistake is silently ignored (the value is undefined). With let and const, the TDZ forces you to fix the problem.

Multiple Variable Declarations and Destructuring Preview

Declaring Multiple Variables

You can declare multiple variables in a single statement, separated by commas:

let firstName = "Alice", lastName = "Smith", age = 30;

However, most style guides recommend one variable per line for readability:

// Preferred: one declaration per line
let firstName = "Alice";
let lastName = "Smith";
let age = 30;

// Also common: one keyword, multiple lines
let firstName = "Alice",
lastName = "Smith",
age = 30;

Destructuring Preview

JavaScript has a powerful shorthand called destructuring that lets you extract values from arrays and objects into separate variables in a single line:

// Array destructuring
const [first, second, third] = [10, 20, 30];
console.log(first); // 10
console.log(second); // 20
console.log(third); // 30

// Object destructuring
const { name, age, city } = { name: "Alice", age: 30, city: "New York" };
console.log(name); // "Alice"
console.log(age); // 30
console.log(city); // "New York"

Destructuring is covered in full detail later in the course. For now, just know it exists and recognize it when you see it in examples.

Swapping Variables

Destructuring provides an elegant way to swap two variables without a temporary variable:

let a = 1;
let b = 2;

// Swap using destructuring
[a, b] = [b, a];

console.log(a); // 2
console.log(b); // 1

Common Mistakes with Variables

Mistake 1: Redeclaring with let

let score = 100;
let score = 200; // SyntaxError: Identifier 'score' has already been declared

Fix: Reassign instead of redeclaring:

let score = 100;
score = 200; // This is reassignment, not redeclaration -> perfectly fine

Mistake 2: Assuming const Makes Objects Immutable

const user = { name: "Alice" };

// Developer expects this to fail, but it works:
user.name = "Bob";
console.log(user.name); // "Bob" (the object was modified!)

const prevents reassigning the variable, not modifying the value it points to.

Fix: If you need true immutability, use Object.freeze():

const user = Object.freeze({ name: "Alice" });
user.name = "Bob"; // Silently fails (TypeError in strict mode)
console.log(user.name); // "Alice"

Mistake 3: Variable Shadowing Confusion

Shadowing occurs when a variable in an inner scope has the same name as one in an outer scope:

let count = 10;

function increment() {
let count = 0; // This shadows the outer 'count' -> it's a NEW variable
count++;
console.log("Inner count:", count); // 1
}

increment();
console.log("Outer count:", count); // 10 (the outer variable was never changed!)

The developer may have intended to modify the outer count, but the inner let count created a completely separate variable.

Fix: If you need to access the outer variable, do not redeclare it:

let count = 10;

function increment() {
count++; // Modifies the outer 'count' (no 'let' keyword here)
console.log("Count:", count); // 11
}

increment();
console.log("Count:", count); // 11

Mistake 4: Using var in Loops with Closures

This is one of the most infamous JavaScript bugs:

for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
}, 100);
}
// Expected: 0, 1, 2
// Actual: 3, 3, 3

Because var is function-scoped, there is only one i shared across all iterations. By the time the setTimeout callbacks execute, the loop has finished and i is 3.

Fix: Use let, which creates a new i for each iteration:

for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
}, 100);
}
// Output: 0, 1, 2

Mistake 5: Forgetting That const Requires Initialization

const username;  // SyntaxError: Missing initializer in const declaration

// This is not allowed because const cannot be assigned later
username = "Alice";

Fix: Always assign a value when declaring with const:

const username = "Alice";

If you do not know the value yet, use let:

let username;
// ... later:
username = getUserInput();

Mistake 6: Accessing Variables Before Declaration

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

console.log(TAX_RATE); // ReferenceError: Cannot access 'TAX_RATE' before initialization
const TAX_RATE = 0.2;

Fix: Always declare variables before using them. Organize your code so declarations come first:

const TAX_RATE = 0.2;
let total = 100;

console.log(total); // 100
console.log(TAX_RATE); // 0.2

Summary

Variables are the building blocks of every JavaScript program. Here is what you need to remember:

  • Use const by default. It prevents accidental reassignment and signals that the binding will not change.
  • Use let when you need to reassign a variable (counters, accumulators, values that change over time).
  • Never use var in new code. It has function scope, allows redeclaration, and causes bugs with closures in loops.
  • const prevents reassignment, not mutation. Objects and arrays declared with const can still have their contents modified.
  • Hoisting moves declarations to the top of their scope. var is initialized as undefined, while let and const enter the Temporal Dead Zone until their declaration line.
  • Use descriptive, camelCase names for variables. Use UPPER_SNAKE_CASE for true constants. Use PascalCase for classes.
  • Watch out for shadowing (same name in inner scope), TDZ errors (using before declaration), and the var closure trap in loops.

With a solid understanding of variables, you are ready to explore the different types of data that variables can hold: numbers, strings, booleans, objects, and more.