Hoisting in JavaScript — what gets hoisted and how
Function declarations are fully hoisted (callable before the line). `var` is hoisted and initialized to undefined. `let`/`const`/`class` are hoisted but uninitialized — accessing them before the declaration throws (Temporal Dead Zone).
"Hoisting" is the casual word for a precise spec behavior: when JavaScript enters a scope (a function body, a module, the global scope), it processes every declaration before it runs any code, creating bindings for them in the scope's environment record. What people call "the variable was hoisted to the top" is really "the binding existed from the start of the scope, but its initialization happens later." The interesting question is not if something is hoisted but when it becomes initialized, and that depends entirely on the declaration kind.
The table of behaviors:
| Declaration | Binding hoisted? | Initialized at top? | Pre-line access |
|---|---|---|---|
function foo() {} | yes | yes — the function itself | works |
var x = 1 | yes | yes (to undefined) | returns undefined |
let x = 1 | yes | no | ReferenceError (TDZ) |
const x = 1 | yes | no | ReferenceError (TDZ) |
class A {} | yes | no | ReferenceError (TDZ) |
function* gen(), async function fn() | yes | yes | works |
Function declarations (function foo() {}) are fully hoisted: both the binding and its function value are available from the top of the enclosing function/module. You can call foo() on line 1 and define it on line 100.
var declarations are hoisted and initialized to undefined. This is the source of the classic "why does console.log(x) log undefined and not ReferenceError?" — the binding exists, just isn't assigned yet.
console.log(x); // undefined
var x = 5;
console.log(x); // 5var is also function-scoped, not block-scoped. It "leaks" out of if/for blocks, which is the cause of the classic loop-closure bug.
let and const declarations are hoisted but not initialized. From the start of the block until the line of declaration, the binding is in the Temporal Dead Zone (TDZ). Any access — read or write — throws ReferenceError. The TDZ exists deliberately to catch typos and out-of-order references that var silently swallowed.
console.log(y); // ReferenceError: Cannot access 'y' before initialization
let y = 5;let and const are block-scoped — they die at the closing } of the block they're in, which is also what makes them safe for for (let i = 0; ...) loop bodies (each iteration gets its own binding).
class declarations behave like let for hoisting: the binding is created at the start of the scope, but the class is not initialized until the line. Accessing before the line is a TDZ error.
Function expressions and arrow functions are NOT hoisted as functions. Only the binding is, and only if it's a var/let/const.
bar(); // TypeError: bar is not a function
var bar = () => 1; // 'bar' exists (undefined) but isn't a function yetIf it were let bar = ..., the call would be a TDZ ReferenceError instead.
The exact output question that file 4 asks:
a = 10;
console.log(a); // 10
var a;Walkthrough: var a is hoisted, so binding a exists from the top of the scope, initialized to undefined. Line 1 assigns 10. Line 2 logs 10. The var a on line 3 doesn't re-declare or reset; it's already been processed in the hoisting pass.
Why TDZ exists. Before let/const (ES5), pre-line access of a var returned undefined, which made typos like if (userName === undefined) silently pass when the variable was declared later. let/const flip this to a runtime error, surfacing the bug early. It also enables temporal initialization patterns like const config = computeConfig(); const derived = config.x; to safely guarantee order.
Hoisting vs scope chain — not the same thing. Hoisting is about when a binding becomes usable within a single scope; scope chain is about which scope a free variable resolves against (lexical scoping). They interact (a hoisted binding shadows a free variable from an outer scope), but they're separate mechanisms.
Practical guidance:
- Prefer
const. Useletwhen reassignment is genuinely needed. Never usevarin new code. - Don't rely on function-declaration hoisting for ordering; put dependencies near their use.
- Top-of-file imports satisfy "declare before use" automatically.
Interview-ready summary. "Every declaration is hoisted as a binding; what differs is initialization. function declarations are initialized immediately. var is initialized to undefined. let/const/class are in the Temporal Dead Zone until their line — accessing them before throws ReferenceError. The TDZ is intentional; it catches a class of bugs var silently allowed."
Code
Follow-up questions
- •What happens in `typeof` on a variable in TDZ?
- •Why does redeclaring `var` succeed but redeclaring `let` throw?
- •How does hoisting interact with default function-parameter values?
Common mistakes
- •Believing `let` and `const` aren't hoisted — they are; only initialization is delayed.
- •Using a function before its `const` binding and getting `not a function`.
- •Assuming class declarations are hoisted like function declarations.
Performance considerations
- •Hoisting has zero runtime cost — it's a parser/scope-setup phase, not a runtime move.
Edge cases
- •`typeof` on a TDZ binding throws (unlike on a never-declared identifier, which returns 'undefined').
- •Function declarations inside blocks have inconsistent hoisting across engines in non-strict mode — avoid them.
Real-world examples
- •ESLint's `no-use-before-define` codifies hoisting hygiene across modern codebases.