Hoisting Get Certified
Hoisting is JavaScript’s behavior of moving declarations to the
top of their scope before the code actually runs. Understanding
it explains why you can sometimes use a function before you write
it, why var gives you undefined instead
of an error, and why let, const, and
classes throw a special “Temporal Dead Zone” error.
Function Declarations Are Fully Hoisted
A function declaration is completely available from the top of its scope. You can call it before the line where it is written.
What this code does: Calls a function before its declaration appears in the code. Because function declarations are fully hoisted, the call works.
console.log(greet("Michael"));
function greet(name) {
return "Hello, " + name;
}
console.log(greet("Michael")) — works even though greet is written below. Function declarations are hoisted with their full body.function greet(name) { return "Hello, " + name; } — the entire definition is moved to the top of the scope before any code runs, so it is usable from line 1.var Is Hoisted and Initialized to undefined
Variables declared with var are hoisted to the top
of their function (or global) scope and automatically given the
value undefined until the assignment line runs.
What this code does: Reads a var
before its assignment. The variable exists but its value is
still undefined.
console.log(city); // undefined (not an error)
var city = "New York";
console.log(city); // "New York"
console.log(city) — does not throw. var is hoisted and initialized to undefined.var city = "New York" — only the declaration is hoisted; the assignment stays here.console.log(city) — now “New York”. This silent undefined is why many prefer let/const.let and const Have a Temporal Dead Zone
let and const are also hoisted, but
they stay uninitialized until their declaration line runs.
Accessing them earlier throws a ReferenceError.
This period is called the Temporal Dead Zone (TDZ).
What this code does: Tries to read a
let variable before its declaration. The Temporal
Dead Zone causes a ReferenceError.
try {
console.log(city);
let city = "New York";
} catch (error) {
console.log("Error:", error.message);
}
try {console.log(city) — let is hoisted but not initialized yet. Accessing it throws ReferenceError (Temporal Dead Zone).let city = "New York" — only after this line runs is city usable.} catch (error) {console.log("Error:", error.message)} — TDZ turns accidental early use into a clear error instead of undefined.
What this code does: Same experiment with
const. It also stays in the Temporal Dead Zone
until the declaration runs.
try {
console.log(country);
const country = "United States";
} catch (error) {
console.log("Error:", error.message);
}
try {console.log(country) — const is also hoisted into the Temporal Dead Zone.const country = "United States"} catch (error) {console.log("Error:", error.message)} — same TDZ rules as let; const cannot be reassigned after initialization either.
The important detail here is that let and
const are hoisted — they are just left
uninitialized until their declaration runs, unlike
var.
Function Expressions Are Not Hoisted Like Declarations
A function expression assigned to a var
follows var’s hoisting rules — the variable itself
is hoisted as undefined, but the function value is
not available until the assignment line runs.
What this code does: Shows that a function
expression assigned to var is only the variable
that is hoisted. Before the assignment, typeof
reports "undefined".
console.log(typeof greet); // "undefined"
var greet = function () {
return "Hi!";
};
console.log(greet()); // "Hi!"
console.log(typeof greet) — “undefined”. Only the var binding is hoisted; the function value is not.var greet = function () { return "Hi!"; } — the function exists only after this line runs.console.log(greet()) — now it works. Function expressions follow the variable’s hoisting rules, not function-declaration rules.Calling It Too Early
Since sayHi is undefined before its
assignment runs, trying to call it as a function throws a
TypeError, not a ReferenceError.
What this code does: Attempts to call a function
expression before its assignment. Because the variable is still
undefined, a TypeError is thrown.
try {
sayHi();
} catch (error) {
console.log("Error:", error.message);
}
var sayHi = function () {
return "Hi!";
};
try {sayHi() — sayHi is hoisted as var (undefined), so calling it throws TypeError: sayHi is not a function.} catch (error) {console.log("Error:", error.message)}var sayHi = function () { return "Hi!"; } — only after this does sayHi become callable.Function Expressions with let
The same function-expression rule applies to let
and const, but because they have a Temporal Dead
Zone, calling them too early throws a
ReferenceError instead of a
TypeError.
What this code does: Tries to call an arrow
function assigned to let before its declaration.
The Temporal Dead Zone causes a ReferenceError.
try {
console.log(sayHello());
} catch (error) {
console.log("Error:", error.message);
}
let sayHello = () => "Hello!";
try {console.log(sayHello()) — let is in the Temporal Dead Zone, so this throws ReferenceError before the arrow is ever created.} catch (error) {console.log("Error:", error.message)}let sayHello = () => "Hello!" — arrow functions are never hoisted as callable; they follow let/const rules.Classes Are Hoisted, But Also Have a TDZ
Like let and const, classes are
hoisted to the top of their scope but remain in the Temporal
Dead Zone until the class declaration itself is evaluated.
What this code does: Attempts to instantiate a class before its declaration. Classes are hoisted but stay in the Temporal Dead Zone, so a ReferenceError is thrown.
try {
const person = new Person("Michael");
console.log(person.name);
} catch (error) {
console.log("Error:", error.message);
}
class Person {
constructor(name) {
this.name = name;
}
}
try {const person = new Person("Michael") — classes are hoisted but stay in the TDZ until the class declaration runs.console.log(person.name)} catch (error) {console.log("Error:", error.message)}class Person { constructor(name) { this.name = name; } } — unlike function declarations, classes cannot be used before this line.var Hoists to the Function, Not the Block
A var declared inside a block, like an
if statement, is hoisted all the way to the top of
the enclosing function — not just to the top of that block.
What this code does: Declares a var
inside an if block. Because var is
function-scoped, the variable is still accessible after the
block.
function demo() {
if (true) {
var message = "Hello from inside the block";
}
console.log(message);
}
demo();
function demo() {if (true) {var message = "Hello from inside the block" — var ignores block boundaries; it is scoped to the whole function.}console.log(message) — still works because var was hoisted to the function, not limited to the if.} demo(); — this is why var in loops/conditionals can leak and cause bugs.let and const Stay Inside Their Block
Unlike var, let and const
do not exist at all outside the block where they were declared.
What this code does: Declares a let
inside a block. Outside that block the variable is not
accessible, so a ReferenceError is thrown.
function demo() {
if (true) {
let message = "Hello from inside the block";
console.log(message);
}
try {
console.log(message);
} catch (error) {
console.log("Error:", error.message);
}
}
demo();
function demo() {if (true) {let message = "Hello from inside the block"console.log(message)}try {console.log(message) — message does not exist outside the if; let stays in its block.} catch (error) { console.log("Error:", error.message); }} demo(); — prefer let/const for predictable block scope.Function Declarations Take Priority Over var
When a var declaration and a function declaration
share the same name, the function declaration’s hoisting wins
first — until the var assignment line actually
runs and overwrites it.
What this code does: Shows that a function
declaration is fully hoisted and wins over a same-named
var until the assignment line runs and overwrites
it.
console.log(typeof example); // "function"
var example = "a string value";
function example() {
return "a function value";
}
console.log(typeof example); // "string"
console.log(typeof example) — “function”. Function declarations are fully hoisted and take priority over a same-named var until the var assignment runs.var example = "a string value" — after this line, example is a string.function example() { return "a function value"; } — already available from the start of the scope.console.log(typeof example) — “string”. Avoid naming a var and a function the same thing in one scope.
At the top, example is already a function, because
function declarations are hoisted with their full definition.
Only after var example = "a string value"; runs
does it become a string.
Hoisting Behavior Summary
| Declaration Type | Hoisted? | Usable Before Its Line? |
|---|---|---|
| Function declaration | Yes | Yes — fully usable |
| var | Yes | Value is undefined until assigned |
| let | Yes | No — Temporal Dead Zone (ReferenceError) |
| const | Yes | No — Temporal Dead Zone (ReferenceError) |
| class | Yes | No — Temporal Dead Zone (ReferenceError) |
| Function expression (var greet = function(){}) | Only the variable, not the function value | No — value is undefined or in the TDZ |
Common Mistakes with Hoisting
- Assuming
letandconstare not hoisted at all — they are, but they stay uninitialized until their declaration runs. - Confusing a function declaration with a function expression and expecting both to be callable before their line.
- Relying on
varhoisting for code that reads more clearly if variables are simply declared at the top. - Forgetting that a
varinside a block is actually scoped to the whole function, which can cause confusing bugs in loops and conditionals. - Assuming classes behave like function declarations — they do not, since they also have a Temporal Dead Zone.
Best Practices for Hoisting
- Declare variables and functions before using them, even though hoisting would sometimes allow otherwise — it keeps code easier to follow.
- Prefer
letandconstovervar; their Temporal Dead Zone helps catch accidental early use as an error instead of silently returningundefined. - Declare each variable in the smallest scope where it is needed, close to where it is first used.
- Avoid giving a function declaration and a variable the same name in the same scope.
- Treat function expressions and arrow functions as unavailable until the line that defines them actually runs.
Quick Summary
- Hoisting moves declarations to the top of their scope before code runs — but not always their assigned values.
- Function declarations are hoisted with their full definition and can be called before their line.
varis hoisted and initialized toundefineduntil assigned.let,const, and classes are hoisted but stay in the Temporal Dead Zone until their declaration runs.- Function expressions and arrow functions follow the hoisting rules of whatever keyword (
var,let, orconst) they are assigned to. varhoists to the enclosing function, whileletandconststay inside their block.
Try It Yourself
Modify the code below. Try changing let to
var to see the error disappear, or moving the
function declaration to see if the order still matters.
What this code does: Practice example that
combines a fully-hoisted function declaration with a
let that is still in the Temporal Dead Zone.
Experiment by swapping keywords or moving lines.
console.log(multiply(2, 3)); // 6 – function is fully hoisted
try {
console.log(count); // ReferenceError (TDZ)
let count = 10;
} catch (error) {
console.log("Error:", error.message);
}
function multiply(a, b) {
return a * b;
}
console.log(multiply(2, 3)) — 6; the function body was hoisted.try {console.log(count) — ReferenceError; let is hoisted but uninitialized.let count = 10} catch (error) {console.log("Error:", error.message)}function multiply(a, b) { return a * b; } — contrasts fully-hoisted functions with TDZ for let.Tip: Remember that “hoisted” does not mean “usable” — function declarations are usable immediately,varis usable butundefined, andlet,const, and classes are hoisted straight into the Temporal Dead Zone.
Understanding hoisting explains behavior you have already seen
with var, let, and const,
and clears up why some errors say
"Cannot access before initialization" instead of
simply "is not defined".
🧪 Test Your JavaScript Code
Edit the code on the left and click “Run Code” to see the console output on the right.
Click “Run Code” to see the output here.
Code Explanation
The complete example stays visible. Use Previous and Next to move through its logical blocks.
Select Next to begin exploring the code.