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.
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
| Declaration | Hoisted? | Usable before the declaration line? |
|---|---|---|
var | Yes | Yes — reads as undefined |
function declaration | Yes, with its body | Yes — fully callable |
function expression (var fn = function(){}) | The variable is; the assignment isn’t | No — the variable is undefined, not a function |
let / const | Yes, but uninitialized | No — throws in the temporal dead zone |
class | Yes, but uninitialized | No — 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.
Tagged
Keep reading
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.
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.
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.