JavaScript Hoisting Table

DeclarationHoisted?Usable before the line?
function f() {}Fully - body and allCallable on lines above its definition
var x = 1Declaration onlyReading before the line gives undefined, not an error
let y = 1Registered, NOT initializedReading before the line throws - the temporal dead zone
const z = 1Registered, NOT initializedSame TDZ as let, plus no reassignment ever
class C {}Registered, NOT initializedTDZ applies - classes are let-like
import * as mHoisted module recordImports resolve before any module code runs
Reference: the MDN hoisting glossary. The accurate mental model is not 'moved to the top' - it is that declarations are REGISTERED when the scope is entered, before any line runs. var registers uninitialized (reads give undefined); let/const/class register uninitialized-but-guarded (reads throw - the temporal dead zone is the error telling you the truth earlier). Bottom line: function declarations are the only fully-hoisted form, which is why calling a helper defined below still works - and why the TDZ is a feature: it converts silent undefined into a loud error. Related tools: closures table (scopes that capture), classes table (TDZ on classes), and error table (the ReferenceError you meet at the boundary).

Hoisting means JavaScript registers declarations when a scope is ENTERED, before any line runs - not that code physically moves to the top. The table below compares the six declaration forms by what happens in that registration phase, and the accurate model replaces the folklore: nothing moves, but the scope knows what exists.

Bottom line: function declarations are the only fully-hoisted form - callable from lines above their definition, which is why helper-below-caller reads fine. var registers as uninitialized (reading before the line gives undefined silently). let, const and class register but stay UNREADABLE until their line - the temporal dead zone - and that error is a feature: it converts a silent undefined into a loud ReferenceError at the exact wrong line.

The honest part: the TDZ sounds like a punishment and works like a guardrail. With var, using a variable before its assignment gives you undefined, and the real bug surfaces later somewhere unrelated. With let, the same mistake throws immediately with the line number where the variable was declared. The dead zone is the language telling you the truth earlier - which is why modern style is let/const everywhere and var only in code reviews of the past.

How to use

  1. Use the table to predict reads-before-writes: callable (function), undefined (var), or TDZ throw (let/const/class).
  2. Order declarations deliberately: helpers above callers work because functions hoist fully; keep const/let above first use as style even though only the TDZ makes it an error.
  3. Read TDZ errors as fixes: 'Cannot access before initialization' names the exact declaration and the exact line - the fastest class of bug to close.

Frequently asked questions

What is the temporal dead zone?

The span between entering a scope and the line where a let/const/class declaration executes. The declaration is already registered (the scope knows the name exists), but reading it throws a ReferenceError instead of giving undefined. After the line runs, the variable is usable. The zone is 'temporal' because it is about TIME (before the line runs), not position: a function defined inside the scope can legally READ the variable before the declaration line, as long as it only CALLS it after.

Why are function declarations fully hoisted but function expressions are not?

A function DECLARATION (function f() {}) registers its entire value - name and body - when the scope is entered, so calling it from an earlier line works. A function EXPRESSION (var f = function() {}) hoists only the var declaration; the assignment happens on its line, so calling f before it gives 'f is not a function' (a TypeError with undefined). The modern same-page convention: declarations for helpers, expressions when the function is data.

Why does var still exist if let is safer?

History and one technical niche. var's loose behavior (function scope, redeclaration allowed, hoisted-undefined reads) made sense in 1995 and breaking it would break the web - so var stays, and let (2015) fixed it by guarding the reads. The niche: some argue var inside try blocks or as explicit 'I want undefined semantics' documentation. In practice, new code uses let/const exclusively, and var in a review is either a compatibility constraint or a mistake.

Does hoisting apply inside classes and blocks?

Classes are hoisted like let: the name is registered but the TDZ guards it - using a class before its declaration line throws. Blocks scope let/const/class declarations (a let inside { } does not exist outside), while var ignores blocks entirely (only function scope bounds it). Imports are their own category: module imports hoist fully like function declarations - the bindings exist before any module code runs, with live links to the exporting module.

Related tools