/glossary/javascript/hoisting
What is hoisting in JavaScript?
- tool
- JavaScript
- versions
- ECMAScript 2015+
- os
- any
Hoisting is the JavaScript behaviour where declarations are processed before any code of their scope runs, so a name can exist before the line that declares it. Function declarations are hoisted with their body and can be called early; var variables are hoisted but stay undefined until assigned; let, const and class are hoisted too, but reading them before the declaration throws a ReferenceError (the temporal dead zone).
See it
cat > hoist.js <<'EOF'
console.log(greet()); // function declarations are hoisted with their body
function greet() { return "hello"; }
console.log(typeof count, count); // var: declared, not yet assigned
var count = 1;
try {
console.log(total); // let: in the temporal dead zone
} catch (e) {
console.log(`${e.name}: ${e.message}`);
}
let total = 2;
try {
sayHi(); // a function expression stored in var
} catch (e) {
console.log(`${e.name}: ${e.message}`);
}
var sayHi = function () {};
EOF
node hoist.jsOutput
hello undefined undefined ReferenceError: Cannot access 'total' before initialization TypeError: sayHi is not a function
Summary
| Declaration | Hoisted | Value before the declaration line |
|---|---|---|
| function f() {} | yes, with the body | the function |
| var x | yes | undefined |
| let x / const x | yes, but uninitialised | ReferenceError (temporal dead zone) |
| class C {} | yes, but uninitialised | ReferenceError (temporal dead zone) |
| var f = function () {} | only the variable | undefined, so calling it throws TypeError |
| import | yes, imports run before the module body | the binding |
Why it happens
Before a function body, module or script starts running, the engine walks its declarations and creates a binding for each name in that scope (the spec calls this declaration instantiation). How the binding starts out depends on the keyword: a function declaration is bound to the finished function object, var is initialised to undefined, and let, const and class are created without a value. Assignments stay where they are written, which is why var count = 1 gives undefined above the line and 1 below it.
var is also function-scoped rather than block-scoped: a var inside an if or for block is hoisted to the enclosing function, while let and const stay inside the block.
The temporal dead zone in practice
The TDZ covers the whole region from the start of the scope to the declaration, so it has effects beyond plain reads:
cat > tdz.js <<'EOF'
try { new Shape(); } catch (e) { console.log(`${e.name}: ${e.message}`); }
class Shape {}
let label = "outer";
function show() {
try { console.log(label); } catch (e) { console.log(`${e.name}: ${e.message}`); }
let label = "inner"; // shadows the outer label for the whole function
}
show();
try { console.log(typeof pending); } catch (e) { console.log(`${e.name}: ${e.message}`); }
let pending = 1;
console.log(typeof neverDeclared);
EOF
node tdz.jsOutput
ReferenceError: Cannot access 'Shape' before initialization ReferenceError: Cannot access 'label' before initialization ReferenceError: Cannot access 'pending' before initialization undefined
- Classes: creating an instance above the
classdeclaration throws, unlike calling a function declared below. - Shadowing: an inner
let labelmakes every use oflabelin that function refer to the inner, not yet initialised binding, even on lines above it. The outer value is not used as a fallback. - typeof:
typeofis safe for names that were never declared (it returns"undefined"), but throws for aletorconststill in its TDZ.
Related errors
Hoisting is behind two common messages. Calling a function expression or arrow function stored in a var before the assignment gives x is not a function, because the variable exists but holds undefined. Using a name with no declaration at all gives x is not defined, which is a different problem from the TDZ message shown above.
FAQ
Are let and const hoisted?
Yes. Their bindings are created when the scope starts, which is why an inner let can shadow an outer variable even on lines above it. They stay uninitialised until the declaration runs, and any access before that throws a ReferenceError.
Does JavaScript physically move declarations to the top?
No. Hoisting is a description of how the engine sets up a scope: it registers all declarations before executing the first statement. The source code is not rearranged.
What is the difference between is not defined and Cannot access before initialization?
x is not defined means no declaration of x exists in any enclosing scope. Cannot access 'x' before initialization means a let, const or class declaration exists but has not run yet.