Tutorials Logic, IN info@tutorialslogic.com

JavaScript Hoisting var, let, const

JavaScript Declaration Timing

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

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.

Javascript Hoisting Worked Example

Javascript Hoisting Worked Example
x = 10;
y = 20;

console.log(x+y); // 30

var x;
var y;

Javascript Hoisting Worked Example 2

Javascript Hoisting Worked Example 2
console.log(x+y); // undefined

var x = 10;
var y = 20;

Javascript Hoisting Worked Example 3

Javascript Hoisting Worked Example 3
console.log(Sum(10, 20)); // 30

var Total = function Sum(x, y) {
	return x + y;
}

Javascript Hoisting Worked Example 4

Javascript Hoisting Worked Example 4
console.log(Sum(10, 20)); // 30

function Sum(x, y) {
	return x + y;
}

let and const - NOT Hoisted

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).

let/const Hoisting

let/const Hoisting
// 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
}

Class Hoisting

Like let and const, class declarations are hoisted but not initialized. You cannot use a class before it is declared.

Class Hoisting - JavaScript Example

Class Hoisting - JavaScript Example
// 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.

Hoisting Summary Table

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)

Module Clarity

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.

Declaration Instantiation

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.

  • Think in binding creation, initialization, and execution phases.
  • Do not interpret early var undefined as a valid value.
  • Expect TDZ access to fail before lexical initialization.
  • Reject duplicate names through linting and review.

Function and Class Declarations

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.

  • Distinguish declarations from expressions assigned to variables.
  • Keep class initialization dependencies explicit.
  • Avoid legacy sloppy block-function behavior.
  • Use forward function calls only when they improve reading order.

Scope and Evaluation Order

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.

  • Check whether an inner TDZ shadows an outer value.
  • Keep parameter defaults independent and ordered.
  • Trace initializer side effects in evaluation order.
  • Separate closure timing from declaration timing.

Modules and Debugging

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.

  • Treat imports as linked live bindings.
  • Inspect the full module graph for cycles.
  • Name the binding state before choosing a fix.
  • Refactor ordering before mechanically replacing var.

Ordering Refactor

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.

  • Construct dependencies before invoking application work.
  • Narrow bindings to the period where their values are valid.
  • Break module cycles through lower-level contracts or dependency injection.
  • Test both isolated modules and the production import graph.
Before you move on

JavaScript Hoisting var, let, const Mastery Check

5 checks
  • 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.
  • Assignments and initializer expressions execute where they appear in the source.
  • Function expressions follow the timing rules of the binding that stores them.
  • JavaScript compiler does not move function expression at the top.
Browse Free Tutorials

Explore 500+ free tutorials across 20+ languages and frameworks.