Articles

What Is Hoisting in JavaScript?

Hoisting moves variable and function declarations to the top of their scope before code runs, which explains some of JavaScript's most confusing bugs.

Takina Takina · · 5 min read
Close-up of JavaScript code on a screen

Hoisting is JavaScript’s behavior of processing variable and function declarations before it executes any code in a scope, which makes it look like those declarations were moved to the top of the file or function — even though the code that references them is written earlier than the declaration itself. It isn’t a special syntax feature; it’s a consequence of how the JavaScript engine parses a script in two passes, and understanding it explains a category of bugs that otherwise look inexplicable.

What actually happens

Before executing a function or module, the JavaScript engine scans it and registers every declaration it finds — var, let, const, function, and class — in that scope’s memory, before running a single line of the code itself. What differs between declaration types is what gets set up during that scan, which is why some hoisted values are usable early and others aren’t.

console.log(hoistedVar); // undefined — no error
var hoistedVar = "I'm here";

This doesn’t throw a ReferenceError, which surprises people expecting hoistedVar to be completely unknown at that point. What actually happened: the engine registered hoistedVar during its initial scan and initialized it to undefined, so by the time console.log runs, the variable exists — it just hasn’t been assigned its real value yet.

Function declarations are hoisted completely

Function declarations are hoisted with their entire body, not just their name, which is why this works:

sayHello(); // "Hello!" — works fine

function sayHello() {
  console.log("Hello!");
}

This is genuinely useful in practice: it means a file can be organized with the “important” top-level logic first and helper function definitions below it, without needing to reorder anything for the code to run.

Function expressions don’t get this treatment, because what’s hoisted is the variable holding the function, not the function itself:

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

var sayHi = function () {
  console.log("Hi!");
};

Here, sayHi is hoisted the same way any var is — initialized to undefined — but the function assignment hasn’t happened yet at the point it’s called, so calling undefined as a function throws.

let and const: hoisted, but not initialized

let and const declarations are hoisted too, in the sense that the engine knows about them from the start of the scope — but unlike var, they aren’t initialized to undefined. Instead, they sit in what’s called the temporal dead zone, a region of code between the start of the scope and the actual declaration line where the variable exists but accessing it throws:

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

This is a deliberate design correction. var’s “hoisted to undefined” behavior lets bugs slip by silently — a typo’d or misordered reference just evaluates to undefined instead of failing loudly. let and const turn that same class of mistake into an immediate, clear error instead. See var vs let vs const for how these declaration types differ beyond hoisting specifically.

Why this trips people up in practice

The classic hoisting bug involves a var inside a loop combined with an async callback:

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// Logs: 3, 3, 3 — not 0, 1, 2

Because var is function-scoped rather than block-scoped, there’s only one i for the entire loop, hoisted to the top of the enclosing function. By the time any of the setTimeout callbacks actually run, the loop has already finished and i has settled at 3. Switching var to let fixes this, because let creates a fresh binding for i on every iteration rather than sharing one hoisted variable across all of them — a difference that has nothing to do with setTimeout itself and everything to do with how each declaration type is scoped and hoisted.

Classes are hoisted like let, not like functions

class declarations are hoisted but land in the temporal dead zone, the same as let and const — despite classes often being thought of as similar to functions:

new Dog(); // ReferenceError: Cannot access 'Dog' before initialization

class Dog {}

This is a common surprise for anyone assuming class declarations behave like function declarations, which are fully usable before their point of definition. They don’t — a class must be declared before it’s referenced, full stop.

A summary of what’s hoisted and how

DeclarationHoisted?Usable before the declaration line?
varYesYes — reads as undefined
function declarationYes, with its bodyYes — fully callable
function expression (var fn = function(){})The variable is; the assignment isn’tNo — the variable is undefined, not a function
let / constYes, but uninitializedNo — throws in the temporal dead zone
classYes, but uninitializedNo — throws in the temporal dead zone

Why this still matters with modern JavaScript

Most style guides today recommend let, const, and function expressions or arrow functions over var and standalone function declarations specifically because they avoid the more confusing edges of hoisting — a let that isn’t ready yet fails loudly instead of silently returning undefined. But hoisting isn’t a legacy quirk that modern syntax opted out of; it’s still exactly how the engine parses every scope, let and const included. Understanding it is less about writing code that deliberately relies on hoisting and more about correctly predicting what a snippet does when you encounter it — particularly the temporal dead zone errors that are one of the more common sources of confusion for anyone newer to the language.

The takeaway

Hoisting is the JavaScript engine registering every declaration in a scope before running any code in it, not literally moving code around. var and function declarations are usable before their written position — var as undefined, functions fully — while let, const, and class are hoisted but stuck in the temporal dead zone until their declaration line actually executes. The practical upshot: prefer let/const and function expressions for the loud-failure behavior they give you, and remember that a ReferenceError about “cannot access before initialization” means the variable exists, it’s just not ready yet.

Takina Takina · · 6 min read

How to Break Up Long Tasks in JavaScript

Long tasks block the main thread for 50ms or more and make pages feel frozen. How to split them with yielding, scheduler.yield(), postTask, and workers.

#JavaScript #Performance #Web Development
Takina Takina · · 5 min read

Microtasks vs Macrotasks in JavaScript, Explained

Microtasks (promise callbacks) run before the next macrotask (timers, events). How the two queues are ordered, and why it matters for your code.

#JavaScript #Web Development #Performance
Takina Takina · · 5 min read

OffscreenCanvas API Explained

OffscreenCanvas lets you render canvas graphics off the main thread, in a web worker, so heavy drawing work stops blocking scrolling, input, and animation.

#JavaScript #Web Development #Performance