JavaScript hoisting is one of those concepts that looks simple at first but can cause confusing bugs when declarations appear to work before they are written. Understanding what the JavaScript engine does during the creation phase makes these behaviors much easier to predict.
Hoisting affects variables, functions, and different declaration types in different ways. In this guide, we will break down the mechanics behind hoisting and use runnable examples to see exactly what happens with var, let, const, function declarations, and the temporal dead zone.
How Hoisting Works in JavaScript
Before JavaScript executes your code line by line, the engine prepares the execution environment and registers declarations. This is why a function declaration can often be called before its position in the source code, while a var variable exists before assignment but initially contains undefined.
The behavior changes with let and const. Their declarations are also processed during environment creation, but they remain inaccessible from the beginning of the scope until execution reaches their declaration; this period is called the Temporal Dead Zone, or TDZ.
Function declarations are fully initialized during the creation phase, which makes them callable before their declaration appears. Function expressions and arrow functions assigned to variables follow the variable declaration rules, so using them before initialization can result in undefined behavior for var or a ReferenceError for let and const.
The most useful way to understand hoisting is to stop thinking of JavaScript as physically moving declarations to the top of the file. Instead, think of the engine creating bindings before executing statements, with different declaration types receiving different initialization behavior.
console.log("=== JavaScript Hoisting Demonstration ===");
console.log("This example shows how different declarations behave before initialization.\n");
// ------------------------------------------------------------
// 1. Function declarations are fully hoisted.
// ------------------------------------------------------------
console.log("1. Function declaration hoisting");
console.log("Calling greet() before its declaration:");
greet();
function greet() {
console.log(" Hello! The function declaration was available before this line.");
}
console.log("Function declarations are initialized during environment setup.\n");
// ------------------------------------------------------------
// 2. var is hoisted and initialized with undefined.
// ------------------------------------------------------------
console.log("2. var hoisting");
console.log("Value of userName before declaration:", userName);
var userName = "Ansh";
console.log("Value of userName after assignment:", userName);
console.log("The declaration existed earlier, but the assignment happened here.\n");
// ------------------------------------------------------------
// 3. let is hoisted but remains in the Temporal Dead Zone.
// ------------------------------------------------------------
console.log("3. let and the Temporal Dead Zone");
try {
console.log(accountBalance);
} catch (error) {
console.log("Accessing let before declaration:");
console.log(" " + error.name + ": " + error.message);
}
let accountBalance = 5000;
console.log("Value after initialization:", accountBalance);
console.log("let cannot be accessed before execution reaches its declaration.\n");
// ------------------------------------------------------------
// 4. const behaves similarly to let.
// ------------------------------------------------------------
console.log("4. const and the Temporal Dead Zone");
try {
console.log(apiVersion);
} catch (error) {
console.log("Accessing const before declaration:");
console.log(" " + error.name + ": " + error.message);
}
const apiVersion = "v1";
console.log("API version after initialization:", apiVersion);
console.log("const must also be initialized before it can be accessed.\n");
// ------------------------------------------------------------
// 5. Function expressions are different from declarations.
// ------------------------------------------------------------
console.log("5. Function expression with var");
try {
calculateTotal();
} catch (error) {
console.log("Calling calculateTotal() before assignment:");
console.log(" " + error.name + ": " + error.message);
}
var calculateTotal = function () {
return 100 + 50;
};
console.log("Calling calculateTotal() after assignment:", calculateTotal());
console.log("The var binding exists early, but the function value is assigned later.\n");
// ------------------------------------------------------------
// 6. Arrow functions follow variable declaration behavior.
// ------------------------------------------------------------
console.log("6. Arrow function with const");
try {
console.log(formatUser("Developer"));
} catch (error) {
console.log("Calling formatUser() before const initialization:");
console.log(" " + error.name + ": " + error.message);
}
const formatUser = (role) => {
return "Role: " + role;
};
console.log("Calling formatUser() after initialization:", formatUser("Developer"));
console.log("Arrow functions do not receive the same hoisting behavior as function declarations.\n");
// ------------------------------------------------------------
// 7. Scope demonstrates why declaration type matters.
// ------------------------------------------------------------
console.log("7. Block scope and hoisting");
var globalStyle = "var is function-scoped";
if (true) {
let blockMessage = "let is block-scoped";
const blockNumber = 42;
console.log("Inside block:", blockMessage);
console.log("Inside block:", blockNumber);
}
console.log("Outside block:", globalStyle);
console.log("let and const bindings cannot be accessed outside their block.\n");
// ------------------------------------------------------------
// 8. Practical rule of thumb.
// ------------------------------------------------------------
console.log("8. Practical hoisting rule");
console.log("Use function declarations when early calls improve readability.");
console.log("Prefer let and const for variables instead of relying on var hoisting.");
console.log("Initialize variables before using them to make execution order obvious.");
console.log("\n=== Hoisting demonstration complete ===");
Conclusion
Hoisting is best understood as the JavaScript engine creating bindings before executing the statements in a scope, rather than as declarations literally being moved to the top. Function declarations are initialized early, var declarations start with undefined, and let and const remain unavailable during the Temporal Dead Zone.
In modern JavaScript, using const by default and let when reassignment is required makes code easier to reason about. Avoid depending on hoisting for normal program flow, and initialize values before using them so the execution order remains clear to both the JavaScript engine and other developers.
Top comments (0)