JavascriptHoisting

JavaScript Hoisting Explained: var, let, const & Functions

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.

Prerequisites for Learning JavaScript Hoisting

A basic grasp of scope and execution contexts will make hoisting much easier to follow.

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.

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 ready

You can call the function early because its declaration is already available the moment the scope is created.

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.

1

Create the execution context

JavaScript creates an execution context for the global script or called function.

2

Register declarations

The runtime creates bindings for variables, functions, classes, and parameters in the appropriate environment.

3

Initialize eligible bindings

Function declarations receive their function objects, while var bindings receive undefined. Lexical bindings remain uninitialized.

4

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 declarations

Think 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, var becomes undefined, 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.

DeclarationBinding created early?Initialized early?Result before declaration
function declarationYesYes, with functionCallable
varYesYes, with undefinedReturns undefined
letYesNoReferenceError
constYesNoReferenceError
class declarationYesNoReferenceError
Function expression with varYesVariable gets undefinedNot callable
Function expression with constYesNoReferenceError
Static importYesLinked before module evaluationAvailable 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: 100

The 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
// admin

This 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: $75

Block 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 data

JavaScript 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 var before 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.

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 started

Mistake 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 created

Alternatively, 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.

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-define and no-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 early undefined values 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 var to let or const can 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.

Keep going with the runtime concepts that make hoisting behavior easier to predict.

JavaScript Scope & Execution Context

Learn how scope controls where declarations are available, and how JavaScript prepares and executes global and function code.

JavaScript Lexical Environment

Explore the environment records that store bindings and make the scope chain work.

Take the JavaScript Hoisting Quiz

Test your ability to predict outputs and identify common hoisting mistakes.

🔑 Key Takeaways

Think of hoisting as scope preparation, not source-code movement, and the rest falls into place.

Test Your JavaScript Hoisting Knowledge

Can you tell which snippets return undefined, throw a ReferenceError, or call a function successfully?

👉 Test your knowledge with our JavaScript Hoisting Quiz