Understanding Variables, Scope, and Hoisting in JavaScript
A clear, practical guide to javascript hoisting, scope, and differences between var, let, and const — with examples, temporal dead zone explanation, and best practices.
Drake Nguyen
Founder · System Architect
Introduction
Variables are the named storage locations in code that hold values you can read or change. In modern JavaScript you declare these with var, let, or const; each keyword affects scope, reassignment rules, and how the engine treats the variable during runtime. This article focuses on javascript hoisting and related concepts like function scope, block scope, and the temporal dead zone so you can write safer, more predictable code.
What is a variable?
A variable is an identifier that points to a value (number, string, object, boolean, null, etc.). A typical javascript variable declaration and initialization looks like this:
// Declare and assign
let username = "sammy_shark";
console.log(username); // sammy_shark
Use descriptive names and follow naming rules: letters, digits, $, and _; cannot start with a digit; case-sensitive; avoid reserved words. Good naming improves readability and reduces bugs in large codebases.
var, let, and const — at a glance
ES6 introduced let and const to address shortcomings of var. Here are the practical differences you need to know:
- var: function scope, hoisted (declaration moves to top), can be redeclared and reassigned.
- let: block scope, not accessible before declaration (temporal dead zone), can be reassigned but not redeclared in the same scope.
- const: block scope, must be initialized when declared, cannot be reassigned (but object properties remain mutable).
Best practice: prefer const for values that shouldn’t be reassigned, use let for variables that will change, and avoid var in new code unless maintaining legacy scripts.
Variable scope explained
Javascript scope determines where a variable is visible. The main scopes are global scope and local scope. Function scope applies to var, while let and const introduce block scope (any {...} block).
// Function-scoped (var)
var species = "human";
function transform() {
var species = "werewolf"; // different from global
console.log(species); // werewolf
}
console.log(species); // human
// Block-scoped (let)
let mood = "calm";
if (true) {
let mood = "wild"; // separate binding
console.log(mood); // wild
}
console.log(mood); // calm
Block scope vs function scope matters when loops and conditionals would otherwise overwrite variables unexpectedly.
Javascript hoisting explained
The term javascript hoisting describes how the engine processes declarations before executing code. Declarations (not initializations) are conceptually moved to the top of their scope. That’s why referencing a var-declared variable before its assignment yields undefined rather than a ReferenceError:
console.log(x); // undefined
var x = 100;
// Internally similar to:
// var x;
// console.log(x);
// x = 100;
let and const have different behavior: accessing them before the declaration throws a ReferenceError because they live in the temporal dead zone (TDZ) from the start of the block until their declaration is evaluated.
console.log(y); // ReferenceError: Cannot access 'y' before initialization
let y = 10;
Hoisting can also affect functions. Function declarations are hoisted with their body, while function expressions assigned to variables follow variable hoisting rules.
Why hoisting matters
Relying on hoisting makes code harder to reason about and can introduce subtle bugs, especially when var is used inside conditional blocks or when duplicate declarations occur. Use let and const to get clearer semantics and to avoid accidental reassignments or redeclarations.
Temporal Dead Zone and errors
The temporal dead zone (TDZ) is the period between entering a scope and the moment a let or const binding is initialized. During the TDZ the binding exists but cannot be used. This prevents accidental reads of uninitialized memory and surfaces bugs earlier.
function example() {
// TDZ for z starts here
// console.log(z); // ReferenceError
let z = 5; // TDZ ends when z is initialized
}
Constants and mutability
const prevents reassignment of the binding, not mutation of the value it references. For primitive values, const makes the value effectively constant. For objects and arrays, properties and elements can still change:
const CAR = { color: 'blue', price: 15000 };
CAR.price = 20000; // allowed
// CAR = {} // TypeError: Assignment to constant variable.
Common pitfalls and examples
- Redeclaration with var silently overwrites a binding; let/const will throw an error in the same scope.
- Using var in loops can leak the loop counter to the outer scope; prefer let for block-local counters.
- Accessing let/const before declaration triggers the temporal dead zone and a ReferenceError.
// var redeclaration
var a = 1;
var a = 2; // allowed
// let redeclaration
let b = 1;
// let b = 2; // SyntaxError
// loop example
for (var i = 0; i < 3; i++) { }
console.log(i); // 3 (leaked)
for (let j = 0; j < 3; j++) { }
// console.log(j); // ReferenceError
Practical guidelines
- Favor const by default; use let when reassignment is needed.
- Avoid var unless you must support legacy code that relies on function scope and hoisting behavior.
- Declare variables as close as possible to where they are used to reduce TDZ surprises and improve readability.
Conclusion
Understanding javascript hoisting and scope (function scope vs block scope) helps you avoid bugs related to unexpected undefined values, redeclarations, and the temporal dead zone. Use let and const to make intent explicit, keep variable declarations localized, and prefer const when possible to produce clearer, safer JavaScript.
Quick tip: If you’re unsure whether a variable should change, start with const — you can always refactor to let later.