Skip to content

/glossary/javascript/hoisting

What is hoisting in JavaScript?

verified onenvironmentlocal · windows 10.0.26200 arm64 · GNU bash 5.3.15 (Git Bash) · v24.20.0updated
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

bash
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.js

Output

hello
undefined undefined
ReferenceError: Cannot access 'total' before initialization
TypeError: sayHi is not a function

Summary

DeclarationHoistedValue before the declaration line
function f() {}yes, with the bodythe function
var xyesundefined
let x / const xyes, but uninitialisedReferenceError (temporal dead zone)
class C {}yes, but uninitialisedReferenceError (temporal dead zone)
var f = function () {}only the variableundefined, so calling it throws TypeError
importyes, imports run before the module bodythe 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:

bash
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.js

Output

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 class declaration throws, unlike calling a function declared below.
  • Shadowing: an inner let label makes every use of label in 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: typeof is safe for names that were never declared (it returns "undefined"), but throws for a let or const still in its TDZ.

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.

Sources