JavaScript Hoisting Explained: var, let, const & Functions

Why can you call some functions before they appear in a file, yet reaching for certain variables too early throws an error? The answer is JavaScript hoisting. Once you understand it, you can predict how declarations behave, sidestep initialization bugs, and confidently answer one of the most common JavaScript interview questions.
What You'll Learn
Here you'll see how JavaScript prepares declarations before it runs a single line of code, and why each declaration type behaves the way it does.
- Why hoisting exists in JavaScript
- How
var,let,const, classes, and functions are hoisted - What the temporal dead zone means
- How execution contexts and lexical environments affect hoisting
- Common hoisting mistakes and safer coding patterns
Prerequisites for Learning JavaScript Hoisting
A basic grasp of scope and execution contexts will make hoisting much easier to follow.
Review these foundational topics if they are unfamiliar:
You can also test your foundation:
A Beginner-Friendly Mental Model for Hoisting
Picture JavaScript walking into a room before a meeting starts. It reads the guest list and sets out a labeled seat for every declaration. Some guests, like function declarations, are seated right away. A var declaration gets a seat with an undefined placeholder card. A let, const, or class declaration gets a reserved seat that no one is allowed to use yet.
Think of each JavaScript scope as a theater.
Before the performance starts, JavaScript reads the cast list and reserves places for all declarations in that scope. Function declarations arrive ready to perform. Variables declared with var receive a temporary card marked undefined.
Variables declared with let or const have reserved places too, but a barrier prevents anyone from accessing them before their declaration is reached. That restricted period is the temporal dead zone.
Nothing in the script is physically moved. JavaScript prepares the scope first and then executes the code from top to bottom.
This picture holds up better than the old "declarations move to the top" line, because nothing in your source code actually moves.
A Real-Life Analogy for Hoisting
Think of a hotel setting up reservations before any guests arrive. Every reservation is recorded up front. Some rooms are ready to enter immediately; others stay locked until check-in.
Hoisting works the same way. JavaScript knows a declaration exists before execution starts, but whether you can actually use it depends on how you declared it.
Why Does JavaScript Hoisting Exist?
Hoisting exists because JavaScript needs to know which bindings and functions live in a scope before it runs any statements there. That preparation lets the runtime resolve names consistently, and it's what makes calling a function declaration before its position in the file possible at all.
Without this setup phase, you'd have to place every function above the code that calls it, every time. The runtime would also know far less about local bindings when it builds an execution context.
None of this comes from a hidden command that shuffles your code around. Hoisting is simply the visible result of how JavaScript's designers defined scope creation and declaration processing. Modern declarations like let and const keep that early scope preparation but block access until the code reaches them.
What Is Hoisting in JavaScript?
JavaScript hoisting is the behavior where declarations are registered in their scope before any code runs. Function declarations are initialized right away, var variables are initialized with undefined, and let, const, and class declarations stay uninitialized until execution reaches them.
showMessage();
function showMessage() {
console.log("JavaScript is ready");
}
// Expected output: JavaScript is readyYou can call the function early because its declaration is already available the moment the scope is created.
“Hoisting” is a teaching term, not a spec term. The ECMAScript specification talks about declaration instantiation and environment records, not about physically moving declarations.
How Does Hoisting Work in JavaScript?
Hoisting works because JavaScript prepares declarations while creating a scope and only then runs the statements inside it. Functions get callable values immediately, var gets undefined, and lexical declarations stay uninitialized. That single difference decides whether accessing a name early succeeds, returns undefined, or throws.
Create the execution context
JavaScript creates an execution context for the global script or called function.
Register declarations
The runtime creates bindings for variables, functions, classes, and parameters in the appropriate environment.
Initialize eligible bindings
Function declarations receive their function objects, while var bindings receive undefined. Lexical bindings remain uninitialized.
Execute statements
JavaScript runs the code from top to bottom and assigns values when it reaches each initializer.
A simplified flow looks like this:
Create scope
↓
Register declarations
↓
Initialize functions and var
↓
Begin statement execution
↓
Initialize let, const, and class at their declarationsThink Like the JavaScript Engine
The engine prepares the current scope before running its statements. That's why a declaration can already exist even though its initializer hasn't run yet.
-
An execution context is created.
Global code creates a global execution context. Calling a function creates another context and places it on the call stack. -
Lexical environments are prepared.
Bindings for parameters, functions, and variables are recorded in environment structures associated with the current scope. -
The scope chain is connected.
If a name is not found locally, JavaScript can search outer lexical environments. -
Declarations receive different initial states.
Function declarations become callable,varbecomesundefined, and lexical declarations remain uninitialized. -
Statements execute in order.
Initializers run only when execution reaches them. -
Finished contexts leave the call stack.
Their memory can later be reclaimed when nothing still references it. Hoisting itself does not keep unused variables alive.
How Each JavaScript Declaration Is Hoisted
Every declaration is registered before execution, but its initialization rules decide what happens when you access it early.
| Declaration | Binding created early? | Initialized early? | Result before declaration |
|---|---|---|---|
function declaration | Yes | Yes, with function | Callable |
var | Yes | Yes, with undefined | Returns undefined |
let | Yes | No | ReferenceError |
const | Yes | No | ReferenceError |
class declaration | Yes | No | ReferenceError |
Function expression with var | Yes | Variable gets undefined | Not callable |
Function expression with const | Yes | No | ReferenceError |
Static import | Yes | Linked before module evaluation | Available through module binding |
Function Declaration Hoisting
A function declaration is fully initialized during scope setup, so you can call it before the line where it's written.
console.log(calculateTotal(20, 5));
function calculateTotal(price, quantity) {
return price * quantity;
}
// Expected output: 100The Difference Between var Hoisting and Assignment
Only the declaration is prepared early. The assignment still runs exactly where it appears in the execution flow.
console.log(userRole);
var userRole = "admin";
console.log(userRole);
// Expected output:
// undefined
// adminThis behaves conceptually like:
var userRole;
console.log(userRole);
userRole = "admin";
console.log(userRole);let, const, and the Temporal Dead Zone
A let or const binding exists from the very start of its block, but you can't touch it until its declaration is evaluated. That off-limits window is called the temporal dead zone, or TDZ.
console.log(accountName); // ReferenceError
const accountName = "Northwind";Rather than quietly handing back undefined, the TDZ surfaces early-access mistakes so you catch them right away.
Function Expressions Are Governed by Their Variables
A function expression doesn't get function-declaration hoisting. Its behavior comes entirely from the variable that holds it.
console.log(formatPrice); // undefined
var formatPrice = function (amount) {
return `$${amount.toFixed(2)}`;
};Call formatPrice() before the assignment and you'll get a TypeError, because its value at that point is still undefined.
Practical JavaScript Hoisting Examples
Here's how hoisting shows up in the code you write every day.
Organizing Helper Functions
Function declarations let you put the main workflow up top and keep the supporting details below it.
const order = {
price: 25,
quantity: 3,
};
displayReceipt(order);
function displayReceipt(currentOrder) {
const total = calculateOrderTotal(currentOrder);
console.log(`Total: $${total}`);
}
function calculateOrderTotal(currentOrder) {
return currentOrder.price * currentOrder.quantity;
}
// Expected output: Total: $75Block Scope and Shadowing
A local declaration shadows an outer variable across the whole block, including the TDZ that comes before the local declaration.
const statusMessage = "Application ready";
function displayStatus() {
// console.log(statusMessage); // ReferenceError
const statusMessage = "Loading user data";
console.log(statusMessage);
}
displayStatus();
// Expected output: Loading user dataJavaScript finds the local binding first. It won't fall back to the outer variable just because the local one hasn't been initialized yet.
How to Recognize Hoisting in Real Code
Hoisting matters whenever a declaration is referenced before its line, or a local declaration shadows an outer name. Watch for var, function declarations, let, const, classes, and static imports near the top of a module or function scope.
Common production patterns include:
- Main functions calling helper declarations written later in the file
- Legacy code using
varbefore assignment - Function expressions stored in variables
- Block-scoped variables that shadow outer configuration values
- Classes referenced before their declarations
- ES modules with static imports resolved before module evaluation
- Tests that import or invoke declarations organized lower in a module
When code gives you undefined, a ReferenceError, or "is not a function," check declaration order and hoisting rules first.
Common Hoisting Mistakes Developers Make
Most hoisting bugs trace back to one thing: confusing declaration setup with value initialization.
Common mistake: A hoisted variable is not necessarily ready to use. JavaScript may know the binding exists while its intended value is still unavailable.
Mistake 1: Treating var as Already Assigned
❌ Wrong
if (isFeatureEnabled) {
startFeature();
}
var isFeatureEnabled = true;
function startFeature() {
console.log("Feature started");
}
// No output because isFeatureEnabled is undefined during the condition.✅ Correct
const isFeatureEnabled = true;
if (isFeatureEnabled) {
startFeature();
}
function startFeature() {
console.log("Feature started");
}
// Expected output: Feature startedMistake 2: Calling a Function Expression Too Early
❌ Wrong
createReport(); // ReferenceError
const createReport = function () {
console.log("Report created");
};✅ Correct
const createReport = function () {
console.log("Report created");
};
createReport();
// Expected output: Report createdAlternatively, use a function declaration when calling before declaration is intentional.
Mistake 3: Assuming typeof Always Avoids Errors
console.log(typeof missingVariable); // "undefined"
// console.log(typeof pendingValue); // ReferenceError
const pendingValue = 10;typeof is safe for an identifier that was never declared, but it still throws when the identifier sits inside its temporal dead zone.
JavaScript Hoisting Best Practices
Clear declaration order beats asking readers to memorize every hoisting rule.
Pick the right keyword: Reach for const by default, use let when you actually need to reassign, and leave var behind in modern JavaScript.
Declare before you use: Keep variables above their first use. Call function declarations ahead of their definition only when it genuinely improves how the file reads.
Additional recommendations include:
- Keep declarations close to the code that uses them.
- Avoid using the same variable name in nested scopes.
- Do not describe hoisting as JavaScript physically moving code.
- Enable ESLint rules such as
no-use-before-defineandno-var. - Choose between function declarations and expressions intentionally.
- Treat TDZ errors as useful signals of incorrect execution order.
Real-World Uses of Hoisting
You rarely set out to "use" hoisting, yet it quietly shapes application structure, module loading, legacy maintenance, and debugging.
- Function organization: Public workflow code can appear before helper function declarations.
- ES modules: Static imports are linked before a module body is evaluated.
- Legacy applications: Older libraries often use
var, making earlyundefinedvalues common. - Testing: Test cases may call helper declarations defined later in the file.
- Debugging: Hoisting knowledge helps distinguish an undeclared name from an uninitialized binding.
- Refactoring: Converting
vartoletorconstcan reveal code that depended on early access.
JavaScript Hoisting Interview Questions
These questions check whether you actually understand declaration initialization, not whether you've memorized a slogan.
Frequently Asked Questions About JavaScript Hoisting
Short answers to the questions that come up most often around hoisting, scope, and the temporal dead zone.
Related JavaScript Topics and Quizzes
Keep going with the runtime concepts that make hoisting behavior easier to predict.
Learn how scope controls where declarations are available, and how JavaScript prepares and executes global and function code.
Explore the environment records that store bindings and make the scope chain work.
🔑 Key Takeaways
Think of hoisting as scope preparation, not source-code movement, and the rest falls into place.
- JavaScript registers declarations before executing statements in a scope.
- Function declarations are initialized early and can be called before their location.
varis initialized withundefined, but its assignment still runs later.let,const, and classes remain inaccessible in the temporal dead zone.- Function expressions follow the hoisting behavior of their containing variable.
- Declaring variables before use produces clearer and safer code.
Test Your JavaScript Hoisting Knowledge
Can you tell which snippets return undefined, throw a ReferenceError, or call a function successfully?