JavaScript creates bindings before executing a scope, but declarations are initialized differently. Function declarations are callable earlier; var is initialized to undefined; let, const, and class bindings remain in the temporal dead zone until their declaration executes.
Hoisting is JavaScript's default behavior of moving function and variable declarations to the top of their scope before code execution. It means, no matter where functions and variables are declared, they are moved to the top of their scope regardless of whether their scope is global or local.
A var binding is initialized before execution, while its assignment still runs in source order.
Function declarations are initialized during declaration instantiation rather than moved as source text.
JavaScript compiler does not move function expression at the top. So, function hoisting in JavaScript is only possible with definition.
x = 10;
y = 20;
console.log(x+y); // 30
var x;
var y;
console.log(x+y); // undefined
var x = 10;
var y = 20;
console.log(Sum(10, 20)); // 30
var Total = function Sum(x, y) {
return x + y;
}
console.log(Sum(10, 20)); // 30
function Sum(x, y) {
return x + y;
}
Variables declared with let and const are hoisted to the top of their block but are NOT initialized. Accessing them before declaration causes a ReferenceError - this is called the Temporal Dead Zone (TDZ).
// var - hoisted and initialized as undefined
console.log(a); // undefined (no error)
var a = 5;
// let - hoisted but NOT initialized (Temporal Dead Zone)
// console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 10;
// const - same as let, must be initialized at declaration
// console.log(c); // ReferenceError
const c = 15;
// Practical example of TDZ
function checkTDZ() {
// TDZ starts here for 'x'
console.log(typeof x); // ReferenceError in strict mode
let x = 'hello'; // TDZ ends here
}
Like let and const, class declarations are hoisted but not initialized. You cannot use a class before it is declared.
// This will throw ReferenceError
// const obj = new Animal(); // ReferenceError
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} makes a sound.`;
}
}
// This works fine - class is declared before use
const dog = new Animal('Dog');
console.log(dog.speak()); // Dog makes a sound.
| Declaration | Hoisted? | Initialized? | Accessible Before Declaration? |
|---|---|---|---|
| var | Yes | Yes (undefined) | Yes (returns undefined) |
| let | Yes | No (TDZ) | No (ReferenceError) |
| const | Yes | No (TDZ) | No (ReferenceError) |
| function declaration | Yes | Yes (full body) | Yes |
| function expression | Partial (var only) | No | No (TypeError) |
| class | Yes | No (TDZ) | No (ReferenceError) |
Do not organize code around reading bindings before their declarations even when a function declaration permits it. Declare dependencies before use and prefer const or let so an early access fails clearly rather than producing undefined.
Imports are statically linked and expose live bindings, but module evaluation order and cycles can still reveal uninitialized values. Break circular dependencies by extracting shared contracts or reversing ownership, not by adding arbitrary delays.
Hoisting is an informal way to describe observable behavior created when JavaScript prepares declarations before executing statements. The engine does not rewrite source by moving lines to the top. It creates environment records and bindings according to the declaration type, then executes code in source order. Reasoning from binding creation and initialization is more accurate than imagining text movement.
A `var` binding is created in the variable environment and initialized to `undefined` before its assignment statement runs. Reading it early therefore produces `undefined`, which can hide an ordering defect. Assigning before the declaration updates the same binding in many function or script cases, but this style is unclear and behaves differently from lexical declarations.
`let` and `const` bindings are created when the lexical environment is instantiated but remain uninitialized until evaluation reaches the declaration. The interval is the temporal dead zone. Access during it throws `ReferenceError`; `typeof` also throws for that uninitialized lexical binding even though `typeof` on a truly undeclared name returns `"undefined"`.
Redeclaration rules differ. A lexical declaration cannot share its scope with conflicting lexical or variable declarations according to the grammar rules, while repeated `var` declarations are often permitted. Treat duplicate names as defects even where syntax allows them, and let a linter detect shadowing and redeclaration before runtime.
A function declaration normally creates and initializes its binding during declaration instantiation, so code in the same valid scope can call it before the declaration statement appears. This supports organizing high-level flow above helper definitions, but excessive forward references can still make dependencies and side effects difficult to follow.
A function expression follows the rules of the variable that receives it. A `var` function expression reads as `undefined` before assignment and fails when called; a `let` or `const` binding is in the temporal dead zone. Arrow syntax does not change that binding behavior.
Class declarations create lexical bindings with temporal dead zone behavior. The class cannot be used before its declaration is evaluated. Extends expressions, computed keys, static fields, and static initialization blocks have their own evaluation order, so avoid class definitions whose initialization depends on later mutable setup.
Block-level function declarations have standardized lexical behavior in strict code, while legacy web semantics can complicate sloppy classic scripts. Do not rely on browser-specific historical behavior. Use modules or strict mode and declare functions at a scope whose visibility is obvious.
Each function call, block, module, and relevant language construct creates or uses an environment according to its rules. An inner binding can shadow an outer binding from the beginning of the inner scope, including its temporal dead zone. Code before the inner declaration cannot fall back to the outer name because resolution already found the uninitialized inner binding.
Default parameter initializers run in their own parameter environment before the function body. A default can reference an earlier parameter but not a later parameter that remains uninitialized. Body declarations are not generally available to parameter initializers. Keep defaults simple or delegate complex setup after entry.
Computed property names, destructuring defaults, field initializers, static blocks, and module top-level statements all execute at defined moments. A bug blamed on hoisting is often an import side effect or initializer that reads state too early. List evaluation order and locate the first read, not only the later failing call.
Callbacks run later but close over bindings, not frozen snapshots. By the time a timer runs, initialization may have completed or a mutable value may have changed. Distinguish early-access errors from later closure values; they involve the same binding but different execution jobs.
Module imports are live read-only views of exported bindings, and static imports are resolved and linked before module evaluation. Imports are not copied snapshots. A module can observe an exported value change after initialization, while assignment to the imported binding itself is not allowed.
Cyclic module graphs can expose a binding before the exporting module initializes it, producing a temporal dead zone error or partially initialized behavior through side effects. Break the cycle by moving shared contracts to a lower module, passing dependencies into functions, or delaying work until after explicit bootstrap. Reordering imports without understanding the graph is fragile.
Debug a suspected hoisting issue by identifying the exact scope, declaration kind, initialization statement, and first access. Pause before the access and inspect whether the binding is missing, undefined, uninitialized, shadowed, or assigned an unexpected value. Those states require different fixes.
Modernization should replace `var` with `let` or `const` only after checking scope and use-before-declaration. A mechanical replacement can turn an early undefined into a ReferenceError, which reveals a real ordering dependency but may break production. Add tests, move initialization before use, narrow scope, and then change the declaration.
Move configuration and dependency creation before the code that consumes them. If two initializers need each other, replace shared mutable module state with an explicit factory or bootstrap function that receives both dependencies after construction. This turns evaluation order into a callable contract and makes partial startup failure testable.
For a large function, narrow variables into the block where their value becomes valid and extract calculations that have complete inputs. Replace sentinel undefined states with an explicit result or state object when “not initialized,” “not found,” and “failed” need different handling. The goal is not merely satisfying a no-use-before-define rule; it is making invalid states unreachable.
When a function declaration is intentionally placed below the high-level flow, keep it pure or clearly named so the forward call has no hidden initialization dependency. If helper creation depends on runtime state, declare a factory and call it after that state exists. Tests should import modules in isolation and through the real entry graph to catch cycle-dependent behavior.
Explore 500+ free tutorials across 20+ languages and frameworks.