javascript - Online Learning - Programming Languages - Tech tutorial

Hoisting in JavaScript

Updated May 2026

I still remember logging a variable in a tiny browser script, expecting a crash, and getting undefined instead. I stared at the console, confused. The variable was declared ten lines below. How could it already exist?

And like most beginners, I memorized the wrong explanation first: “JavaScript moves your declarations to the top of the file.” It does not. No code moves anywhere.

When JavaScript runs your code, it processes declarations before executing anything line by line. But each declaration type behaves differently during that early pass:

  • Function declarations are fully available before their definition line.
  • var is hoisted and initialized to undefined.
  • let, const, and class are hoisted but cannot be accessed before their declaration line runs.

The phrase “moved to the top” is a mental model. It does not describe what the engine literally does. It is a useful shorthand. Just understand where it breaks down.

What Hoisting Means in JavaScript

Hoisting in JavaScript describes the engine’s behavior of registering declarations during a setup phase before any code executes.

The key distinction is between declaration and initialization:

  • Declaration means the name is registered in the current scope.
  • Initialization means the variable receives a usable value.
var a;    // declaration (a exists, value is undefined)
a = 3;    // initialization (a now holds 3)

Those two steps happen at different times. Hoisting is about the first step happening earlier than you might expect based on reading the code top to bottom.

The ECMAScript specification does not define a formal feature called ‘hoisting,’ though the behavior emerges from declaration processing rules. It is a term developers use to describe observable behavior. You do not “turn on” hoisting or opt into it. It is always happening. Understanding what hoisting does to each declaration type matters more than knowing it exists.

Declarations in JavaScript

A declaration in JavaScript introduces a name into the current scope. The declaration types relevant to hoisting are:

function greet() {
  return "hello";
}

var score = 10;
let name = "Ada";
const MAX = 100;

class User {
  constructor(name) {
    this.name = name;
  }
}

The important question is not whether something is declared. It is what state the declaration is in before its line runs.

Why Hoisting Confuses So Many Developers

Most confusion comes from one sentence repeated across tutorials: “JavaScript moves declarations to the top.” That makes “hoisted” sound like “available.” It is only partly true.

var is hoisted and initialized to undefined, so accessing it early does not crash your program. It just gives you a value you probably did not expect. let, const, and class are also hoisted, but they sit in an inaccessible state until their declaration line actually executes. Accessing them early throws a ReferenceError.

What state is the name in before the declaration line executes?

Two errors to keep separate:

  • undefined means the variable exists but has not been assigned a value yet.
  • ReferenceError means the variable exists in scope but is not accessible yet (temporal dead zone), or it was never declared in that scope at all.

Quick Comparison of Hoisting Behavior

How Different JavaScript Declarations Behave Before Their Definition

Declaration TypeHoisted?Accessible Before Declaration?What Happens If Accessed Early?Scope
Function declarationYesYesWorks normallyFunction/global
Function expression (var)Yes (variable only)NoTypeError: not a functionFunction/global
Function expression (let)Yes (variable only)NoReferenceErrorBlock
Function expression (const)Yes (variable only)NoReferenceErrorBlock
varYesYes (as undefined)Returns undefinedFunction/global
letYesNo (TDZ)ReferenceErrorBlock
constYesNo (TDZ)ReferenceErrorBlock
classYesNo (TDZ)ReferenceErrorBlock

The pattern: function declarations are the only type that is fully usable before its definition line. Everything else has restrictions.

Function Hoisting

Function declarations are fully hoisted within their scope. Both the name and the function body are available before the definition line.

console.log(greeting()); // "Hello there!"

function greeting() {
  return "Hello there!";
}

The engine registers the entire function during the setup phase. Calling a function name that was never declared throws a ReferenceError.

Function hoisting is scoped. A function declared inside another function is hoisted to the top of that enclosing function, not to the global scope.

function outer() {
  inner(); // works here

  function inner() {
    console.log("inside outer");
  }
}

inner(); // ReferenceError

I almost never rely on function hoisting on purpose. It works. Reading top-down is easier for most teams. You will still see this pattern in older codebases. Developers defined helper functions at the bottom of a file and called them at the top. Understanding why that code runs without errors helps when debugging legacy code.

Variable Hoisting With Var

var declarations are hoisted and initialized to undefined. The assignment stays where you wrote it.

console.log(a); // undefined
var a = "a";
console.log(a); // "a"

JavaScript hoists the declaration, not the assignment. On the first log, a exists but holds undefined. After the assignment line runs, it holds "a".

var is function-scoped, not block-scoped. This means it can leak out of blocks:

if (true) {
  var message = "hello";
}

console.log(message); // "hello"

var hides timing mistakes. The variable exists in a wider scope than the block suggests. Accessing it before assignment silently returns undefined instead of throwing an error.

Variable Hoisting With Let And Const

let and const are hoisted too. But they are not initialized when the scope starts. They sit in the temporal dead zone (TDZ) from the beginning of the scope until the declaration line executes.

The TDZ starts when the scope begins. It ends when the declaration line runs. Any access during that window throws a ReferenceError.

console.log(a); // ReferenceError: Cannot access 'a' before initialization
let a = "a";

console.log(count); // ReferenceError
const count = 5;

The error messages differ. A variable in the TDZ produces Cannot access before initialization. A variable that was never declared at all produces is not defined. Both throw ReferenceError. The messages point to different causes.

Some developers argue that let and const are “non-hoisting” in the strictest practical sense. You can never use them before their declaration line. The engine does register their names early. But in practice, they behave as if they do not exist until you reach them.

I prefer the stricter behavior of let and const. It catches bugs earlier. Getting an error is better than silently working with undefined.

Class Hoisting

Class declarations behave like let and const, not like function declarations.

let instance = new MyClass(); // ReferenceError: Cannot access 'MyClass' before initialization

class MyClass {
  constructor() {
    this.value = 1;
  }
}

The name is registered early. The class is not usable until the declaration line executes.

This surprises developers coming from older constructor-function patterns. function Person() {} was fully hoisted. Switching to class Person {} changes the hoisting behavior entirely.

Function Expressions And Arrow Functions

A function declaration defines a standalone function. A function expression assigns a function to a variable. That distinction changes hoisting behavior completely.

With a function expression, the function itself is not what gets hoisted. The variable name is what follows var, let, or const hoisting rules.

greet(); // works

function greet() {
  return "hello";
}

sayHello(); // TypeError: sayHello is not a function

var sayHello = function () {
  return "hello";
};

var sayHello is hoisted and initialized to undefined. Calling undefined() throws a TypeError, not a ReferenceError. The variable exists. It is not a function yet.

Arrow functions are expressions, not declarations. They follow the same rules:

sayHi(); // ReferenceError: Cannot access 'sayHi' before initialization

const sayHi = () => "hi";

const sayHi is in the temporal dead zone. The access throws a ReferenceError before JavaScript checks whether the value is callable.

This pattern appears frequently in interviews and debugging sessions. The error type tells you what went wrong. TypeError means the variable exists but is not a function. ReferenceError means the variable is not accessible yet.

Hoisting Is Relative To Scope

Hoisting happens within the current scope, not across all scopes. A declaration can be hoisted. It still remains completely inaccessible outside the scope where it was defined.

function test() {
  console.log(value); // undefined
  var value = 10;
}

test();
console.log(value); // ReferenceError

Inside test(), var value is hoisted to the top of the function. Outside test(), value does not exist at all.

Block scope works the same way for let and const:

{
  console.log(count); // ReferenceError
  let count = 1;
}

“Hoisted” does not mean “global.” It means the declaration is registered at the top of its own scope. That scope is the boundary.

Common Hoisting Bugs In Real Code

What undefined Usually Points To

A variable logging undefined when you expected a value is likely a var accessed before its assignment line. The declaration was hoisted. The assignment had not run yet.

What ReferenceError Usually Points To

A ReferenceError usually means one of two things. Either the variable is in the temporal dead zone (let, const, or class accessed before its declaration line). Or the name does not exist in that scope.

Check the error message. Cannot access before initialization points to TDZ. is not defined points to a missing declaration.

What TypeError: x Is Not a Function Usually Points To

This typically means a function expression was stored in a var. It was called before the assignment ran. The variable is undefined at that point. Calling undefined() throws a TypeError.

Why var In Loops Causes Old-School Bugs

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// 3, 3, 3

var is function-scoped. There is only one i shared across all iterations. By the time the callbacks fire, the loop has finished. i is 3. Replacing var with let fixes this. let creates a new binding per iteration.

To Hoist Or Not To Hoist?

Hoisting is something to understand. It is not something to rely on as a design pattern. The ECMAScript specification does not frame hoisting as a feature developers should leverage. It is a side effect of how the engine processes code.

Some older codebases use function declarations before their definitions deliberately. It reads fine once you know the rules. Most modern style guides discourage relying on hoisting for code organization. The Airbnb JavaScript Style Guide is one example.

Understand hoisting for debugging. Do not use it as your organizing principle.

Best Practices For Writing Clearer JavaScript

  • Prefer const by default. It signals that the binding will not change. This makes code easier to reason about.
  • Use let when reassignment is required. Reserve it for counters, accumulators, or values that genuinely need to change.
  • Avoid var in modern JavaScript. Its function-scoping creates bugs. Its silent undefined initialization creates more. let and const prevent both.
  • Declare variables close to where they are used. This reduces the distance between declaration and usage. Hoisting behavior becomes irrelevant in practice.
  • Do not rely on hoisting to make code “work.” Code that only runs because of hoisting will confuse the next person who reads it.
  • Place function declarations where readers expect them when readability matters.
  • Read the exact error message before debugging blindly. undefined, ReferenceError, and TypeError each point to different causes.

Avoiding var is the easiest way to remove an entire category of confusion in modern JavaScript.

Quick Recap

  • Function declarations are fully hoisted within their scope.
  • var is hoisted and initialized to undefined.
  • let, const, and class are hoisted but stay inaccessible until their declaration line runs (temporal dead zone).
  • Function expressions and arrow functions follow the hoisting behavior of the variable storing them.
  • Hoisting happens within scope, not across all scopes.
  • Understanding hoisting helps most when debugging.

Learn JavaScript By Building Real Projects

Understanding hoisting in JavaScript is one piece of a larger skill set. That skill set includes reading code accurately, debugging confidently, and writing JavaScript that behaves the way you expect. Scope, closures, async patterns, and the event loop all connect to this foundation.

To apply these concepts in real projects, consider joining our specialized courses. Udacity’s programming courses and Nanodegree programs include projects like building APIs, debugging production code, and implementing data structures. You move from understanding concepts to applying them in hands-on projects that build JavaScript skills for real work.