Understanding JavaScript Hoisting for Beginners
Learn how JavaScript hoisting works for var, let, const, functions, and classes. Covers the temporal dead zone, function declaration vs expression hoisting, best practices, and common hoisting mistakes with clear examples.
Hoisting is JavaScript's behavior of moving variable and function declarations to the top of their scope before code execution. This means you can use certain variables and functions before they appear in your code. However, hoisting behaves differently for var, let, const, function declarations, and function expressions. Understanding these differences prevents confusing bugs and helps you write predictable code.
What is Hoisting?
During the compilation phase (before execution), JavaScript scans your code and registers all declarations. This process "hoists" declarations to the top of their scope, but only the declarations - not the assignments:
console.log(message); // undefined (not an error!)
var message = "Hello";
console.log(message); // "Hello"JavaScript sees this as:
var message; // declaration is hoisted to the top
console.log(message); // undefined (declared but not assigned)
message = "Hello"; // assignment stays in place
console.log(message); // "Hello"Hoisting is a Mental Model
No code physically moves. The JavaScript engine creates variables in memory during the compile phase, before executing any code. "Hoisting" is the metaphor developers use to describe this behavior.
var Hoisting
var declarations are hoisted to the top of their function scope and initialized with undefined:
function example() {
console.log(x); // undefined (hoisted, initialized as undefined)
var x = 10;
console.log(x); // 10
if (true) {
var y = 20; // var is function-scoped, not block-scoped
}
console.log(y); // 20 (accessible outside the if block)
}
example();var Hoisting Across Blocks
Because var is function-scoped, declarations inside blocks are hoisted to the function level:
function loopExample() {
console.log(i); // undefined (hoisted from the for loop)
for (var i = 0; i < 3; i++) {
// loop body
}
console.log(i); // 3 (accessible after the loop)
}Multiple var Declarations
function multiVar() {
var x = 1;
var x = 2; // no error - var allows redeclaration
var x = 3;
console.log(x); // 3
}let and const Hoisting
let and const are also hoisted, but they are NOT initialized. They enter a Temporal Dead Zone (TDZ) from the start of their scope until the declaration is reached:
// TDZ starts for 'name'
console.log(name); // ReferenceError: Cannot access 'name' before initialization
let name = "Alice"; // TDZ ends here
// Same behavior with const
console.log(age); // ReferenceError: Cannot access 'age' before initialization
const age = 25;The Temporal Dead Zone (TDZ)
The TDZ is the period between entering a scope and the point where the variable is declared:
{
// TDZ for 'x' starts here
console.log(typeof x); // ReferenceError (not even typeof works in TDZ)
let x = 10; // TDZ ends, x is initialized
console.log(x); // 10
}Compare with var:
{
console.log(typeof y); // "undefined" (var has no TDZ)
var y = 10;
}TDZ with Function Parameters
// TDZ exists in default parameter expressions
function example(a = b, b = 1) {
// 'b' is in the TDZ when 'a' tries to use it
return [a, b];
}
example(); // ReferenceError: Cannot access 'b' before initialization
example(5); // [5, 1] (a gets 5, b gets 1)| Keyword | Hoisted? | Initialized? | TDZ? | Scope |
|---|---|---|---|---|
var | Yes | Yes (undefined) | No | Function |
let | Yes | No | Yes | Block |
const | Yes | No | Yes | Block |
Function Declaration Hoisting
Function declarations are fully hoisted - both the name and the entire function body are available from the top of the scope:
// This works! Function declarations are fully hoisted
greet("Alice"); // "Hello, Alice!"
function greet(name) {
return `Hello, ${name}!`;
}This is one of the most useful aspects of hoisting. You can organize code with helper functions at the bottom and main logic at the top:
// Main logic first (reads top-down)
function main() {
const data = fetchData();
const processed = processData(data);
displayResults(processed);
}
// Helper functions below
function fetchData() { /* ... */ }
function processData(data) { /* ... */ }
function displayResults(results) { /* ... */ }Function Declarations in Blocks
Function declarations inside blocks have inconsistent behavior across browsers. Avoid this pattern:
// DO NOT DO THIS - behavior varies by browser and strict mode
if (true) {
function test() {
return "inside if";
}
}
// Use function expressions instead
let test;
if (true) {
test = function () {
return "inside if";
};
}Function Expression Hoisting
Function expressions follow the hoisting rules of their declaration keyword (var, let, or const). The function itself is NOT hoisted:
// var function expression: variable hoisted, function NOT hoisted
console.log(typeof greetVar); // "undefined"
greetVar("Alice"); // TypeError: greetVar is not a function
var greetVar = function (name) {
return `Hello, ${name}!`;
};
// let function expression: TDZ applies
greetLet("Bob"); // ReferenceError: Cannot access 'greetLet' before initialization
let greetLet = function (name) {
return `Hi, ${name}!`;
};Arrow Function Hoisting
Arrow functions are always expressions, so they follow the same rules as function expressions:
// Arrow functions are NOT hoisted
double(5); // ReferenceError (if const/let) or TypeError (if var)
const double = (x) => x * 2;| Function type | Hoisting behavior |
|---|---|
function greet() {} | Fully hoisted (name + body) |
var greet = function() {} | Name hoisted as undefined, body NOT hoisted |
let/const greet = function() {} | Name hoisted into TDZ, body NOT hoisted |
var greet = () => {} | Name hoisted as undefined, body NOT hoisted |
let/const greet = () => {} | Name hoisted into TDZ, body NOT hoisted |
Class Hoisting
Classes are hoisted like let - into the TDZ:
// Cannot use before declaration
const instance = new MyClass(); // ReferenceError
class MyClass {
constructor() {
this.name = "instance";
}
}
// Class expressions follow the same rules
const instance2 = new MyExpr(); // ReferenceError
const MyExpr = class {
constructor() {
this.name = "instance";
}
};Hoisting Order and Priority
When both a var variable and a function declaration have the same name, the function declaration takes priority:
console.log(typeof example); // "function" (not "undefined")
var example = "I am a string";
function example() {
return "I am a function";
}
console.log(typeof example); // "string" (var assignment overwrites)JavaScript processes this as:
// 1. Function declaration hoisted first
function example() {
return "I am a function";
}
// 2. var declaration hoisted (but ignored because example already exists)
// var example; // no effect
// 3. Execution begins
console.log(typeof example); // "function"
// 4. Assignment executes
example = "I am a string";
console.log(typeof example); // "string"Practical Examples
Safe Initialization Pattern
// RISKY: depends on var hoisting
function processOrders(orders) {
for (var i = 0; i < orders.length; i++) {
var order = orders[i];
var total = calculateTotal(order);
console.log(total);
}
// Bug: i, order, and total are still accessible here
console.log(i); // orders.length
console.log(order); // last order
console.log(total); // last total
}
// SAFE: let/const contain variables properly
function processOrders(orders) {
for (let i = 0; i < orders.length; i++) {
const order = orders[i];
const total = calculateTotal(order);
console.log(total);
}
// i, order, and total are NOT accessible here
}Function Organization with Hoisting
// This works because function declarations are fully hoisted
function app() {
// High-level flow reads clearly
const config = loadConfig();
const data = validate(config);
return format(data);
// Implementation details below
function loadConfig() {
return { debug: false, version: "1.0" };
}
function validate(cfg) {
if (!cfg.version) throw new Error("Version required");
return cfg;
}
function format(data) {
return JSON.stringify(data, null, 2);
}
}Common Hoisting Mistakes
1. Assuming let/const Work Like var
function example() {
// With var, this works due to hoisting
console.log(a); // undefined
var a = 5;
// With let, this throws
console.log(b); // ReferenceError
let b = 10;
}2. Calling Function Expressions Before Declaration
// MISTAKE: treating a function expression like a declaration
calculateTax(100); // TypeError: calculateTax is not a function
var calculateTax = function (amount) {
return amount * 0.08;
};
// FIX: use a function declaration
function calculateTax(amount) {
return amount * 0.08;
}
calculateTax(100); // 83. Relying on var Hoisting in Conditionals
function getDiscount(isMember) {
if (isMember) {
var discount = 0.2;
}
return discount; // undefined if not a member (not an error due to var hoisting!)
}
// Better: explicit initialization
function getDiscount(isMember) {
let discount = 0;
if (isMember) {
discount = 0.2;
}
return discount;
}4. Hoisting Confusion in Switch Statements
// BUG: all cases share the same scope with var
function describe(type) {
switch (type) {
case "a":
var message = "Type A"; // hoisted to function scope
break;
case "b":
var message = "Type B"; // redeclares (no error with var)
break;
}
return message;
}
// FIX: use let/const with block scope
function describe(type) {
switch (type) {
case "a": {
const message = "Type A";
return message;
}
case "b": {
const message = "Type B";
return message;
}
default:
return "Unknown";
}
}Best Practices
| Practice | Why |
|---|---|
Use const by default | Block-scoped, no reassignment, no hoisting confusion |
Use let when you must reassign | Block-scoped, prevents accidental hoisting issues |
Avoid var | Function-scoped, hoisted with undefined, causes subtle bugs |
| Declare variables at the top | Makes the scope explicit and readable |
| Use function declarations for named functions | Fully hoisted, allows top-down code organization |
| Use function expressions for callbacks | Arrow functions are concise and scoped to their block |
| Enable strict mode | Catches undeclared variable assignments |
How the JavaScript Engine Processes Code
Understanding the two-phase process helps:
Phase 1 - Creation (Compile Phase):
- Create the global execution context
- Scan for all
vardeclarations - create them with valueundefined - Scan for all function declarations - create them with the full function body
- Scan for all
let/constdeclarations - register them but leave uninitialized (TDZ)
Phase 2 - Execution:
- Execute code line by line
- Assign values when assignment expressions are reached
- Throw
ReferenceErrorif accessinglet/constin TDZ
// What the engine sees during creation phase:
// 1. var greeting -> undefined
// 2. function sayHi() { ... } -> fully available
// 3. let name -> uninitialized (TDZ)
// Execution phase:
console.log(greeting); // undefined (phase 1 set it)
sayHi(); // works (phase 1 set it)
// console.log(name); // would throw ReferenceError (TDZ)
var greeting = "hello";
function sayHi() {
console.log("hi!");
}
let name = "Alice";
console.log(name); // "Alice" (TDZ is over)Rune AI
Key Insights
- var is hoisted and initialized: accessible before declaration as
undefined - let/const are hoisted but NOT initialized: accessing them before declaration throws
ReferenceError(TDZ) - Function declarations are fully hoisted: name and body available from the top of the scope
- Function expressions are NOT fully hoisted: only the variable name is hoisted
- Use const/let over var: block scope and TDZ prevent accidental hoisting bugs
- Declare at the top: make your code's scope explicit regardless of hoisting
Frequently Asked Questions
Is hoisting a good feature?
Does hoisting happen in modules?
Does async/await affect hoisting?
Why does typeof not throw for undeclared variables but throws in the TDZ?
Conclusion
Hoisting moves declarations to the top of their scope during compilation. var is hoisted and initialized as undefined. let and const are hoisted but enter the Temporal Dead Zone until their declaration is reached. Function declarations are fully hoisted with their body. Function expressions and arrow functions follow the hoisting rules of their declaration keyword. Use const by default, let when reassignment is needed, and avoid var to prevent hoisting-related bugs. Place declarations at the top of their scope and use function declarations for helper functions that benefit from being called before their position in the code.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.