Dynavo Labs
HomeAbout Us
Service
Case StudiesInsightsContact
Get Quote
Dynavo Labs

We design and build digital products that grow businesses — websites, mobile apps, and everything in between.

Services

  • Software Development
  • Web Development
  • Mobile Development
  • AI Development
  • Quality Assurance
  • DevOps & Cloud

Company

  • About
  • Industries
  • Technology
  • Contact

Resources

  • Insights
  • Case Studies

© 2026 dynavo Labs. All rights reserved.

Privacy PolicyTerms & Conditions
dynavo Labsdynavo Labsdynavo Labsdynavo Labsdynavo Labsdynavo Labsdynavo Labsdynavo Labsdynavo Labsdynavo Labsdynavo Labsdynavo Labsdynavo Labsdynavo Labsdynavo Labsdynavo Labs

JavaScript Hoisting and the Temporal Dead Zone

How hoisting works for var, let, const, functions and classes in JavaScript, what the temporal dead zone is, and how to avoid the bugs it causes.

By Muzammil•October 31, 2026•6 min read•Software Development•Frontend
JavaScript Hoisting and the Temporal Dead Zone

This code runs without an error:

console.log(count) // undefined
var count = 5

This code throws one:

console.log(total) // ReferenceError: Cannot access 'total' before initialization
let total = 5

Both lines read a variable before it is declared. The difference comes from hoisting and the temporal dead zone. This guide explains both, one declaration type at a time.

What Is Hoisting?

Before JavaScript runs a scope, it scans the code and registers every declaration in it. The declarations are known before the first line executes. People describe this as declarations being "moved to the top", but nothing moves. The engine creates the bindings first, then runs the code in order.

What differs between declaration types is what state the binding is in once it is created.

DeclarationCreated at scope startInitial valueUsable before its line?
varYesundefinedYes, but reads undefined
function declarationYesThe function itselfYes
letYesUninitializedNo, throws
constYesUninitializedNo, throws
classYesUninitializedNo, throws

Hoisting with var

A var binding exists from the start of its function, with the value undefined. Assignment happens only when execution reaches the line.

function demo() {
  console.log(name) // undefined
  var name = 'Ada'
  console.log(name) // 'Ada'
}

The engine treats this as if the declaration were at the top and the assignment stayed where it was:

function demo() {
  var name
  console.log(name)
  name = 'Ada'
  console.log(name)
}

var is scoped to the function, not the block. A var inside an if or for is visible in the whole function, which is a source of bugs.

Hoisting with Functions

A function declaration is hoisted with its body, so you can call it before it appears in the file:

greet() // 'Hello'

function greet() {
  console.log('Hello')
}

A function expression follows the rules of the variable that holds it:

sayHi() // TypeError: sayHi is not a function

var sayHi = function () {
  console.log('Hi')
}

With var, sayHi exists but is undefined, so calling it is a TypeError. With let or const, the same call is a ReferenceError. Arrow functions assigned to a variable behave the same way.

What Is the Temporal Dead Zone?

let, const and class bindings are also created at the start of their scope. The difference is that they stay uninitialized until execution reaches their declaration. The period between the start of the scope and that line is the temporal dead zone, or TDZ. Touching the variable during that period throws a ReferenceError.

{
  // TDZ for `price` starts here
  console.log(price) // ReferenceError
  const price = 20
  // TDZ ends at the declaration
  console.log(price) // 20
}

The word "temporal" matters. The zone is defined by time, meaning execution order, and not by position in the file. This function looks wrong and works:

const getTax = () => rate * 0.2
const rate = 100

console.log(getTax()) // 20

getTax mentions rate before its declaration in the file, but it is called after rate is initialized, so there is no error.

Proof That let Is Hoisted

If let were not hoisted, the inner declaration below would not matter and the first log would print 'outer':

const label = 'outer'

function show() {
  console.log(label) // ReferenceError
  const label = 'inner'
}

show()

The inner label is registered at the start of the function, so it shadows the outer one for the whole function body, and the early read lands in its TDZ. A binding that was not hoisted could not shadow anything.

The TDZ and typeof

typeof is usually safe on an undeclared name and returns 'undefined'. It is not safe inside a TDZ:

console.log(typeof missing) // 'undefined'

console.log(typeof early)   // ReferenceError
let early = 1

Classes Are in the TDZ Too

const user = new User('Ada') // ReferenceError

class User {
  constructor(name) {
    this.name = name
  }
}

Unlike function declarations, class declarations are not usable before their definition.

A Classic Bug: var in Loops

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0)
}
// 3, 3, 3

There is one i for the whole function, and the timers run after the loop finishes. With let, each iteration gets its own binding:

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0)
}
// 0, 1, 2

Why the TDZ Exists

The TDZ turns a silent bug into a loud one. With var, reading a variable too early returns undefined and the program continues with bad data. With let and const, you get an error that names the variable and points at the line. It also makes const meaningful, since a constant that could be read before it had its value would not be constant.

Practical Rules

  • Use const by default, let when you reassign. Avoid var in new code.
  • Declare variables at the top of the scope where you use them. This removes most TDZ errors by construction.
  • Define classes and constants before the code that uses them at load time. Functions that use them later are fine.
  • Prefer function declarations for helpers you want to call before their definition. Or place the helper above its first use.
  • Turn on a linter rule such as no-use-before-define. It catches the early reads.

Common Questions

Is let hoisted? Yes. The binding is created at the start of the scope, but it is uninitialized until its declaration runs.

Why does var return undefined and let throw? var bindings are initialized to undefined when created. let bindings are left uninitialized on purpose.

Does the TDZ affect function parameters? Yes, in one case. A default parameter that refers to a later parameter throws, as in function f(a = b, b) {}.

Summary

Every declaration is hoisted, and what changes is its initial state. var starts as undefined, function declarations start fully defined, and let, const and class start uninitialized until their line runs. The time in between is the temporal dead zone, and it exists to turn silent bugs into errors you can read.

#JavaScript#Hoisting#Temporal Dead Zone#var let const#Scope#Web Development

Responses

M↓

Get Started on a Project?

Let's Talk

Start the
Journey
dynavolabs.info@gmail.comStart the Journey