docs/beyond/concepts/hoisting.mdx
Why can you call a function before it appears in your code? Why does var give you undefined instead of an error, while let throws a ReferenceError? How does JavaScript seem to know about variables before they're declared?
// This works - but how?
sayHello() // "Hello!"
function sayHello() {
console.log("Hello!")
}
// This doesn't throw an error - why?
console.log(name) // undefined
var name = "Alice"
// But this does throw an error - what's different?
console.log(age) // ReferenceError: Cannot access 'age' before initialization
let age = 25
The answer is hoisting. It's one of JavaScript's most misunderstood behaviors, and understanding it is key to writing predictable code and debugging confusing errors.
<Info> **What you'll learn in this guide:** - What hoisting actually is (and what it isn't) - How [`var`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/var), [`let`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let), and [`const`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const) are hoisted differently - Why function declarations can be called before they appear in code - The Temporal Dead Zone and why it exists - Class and import hoisting behavior - Common hoisting pitfalls and how to avoid them - Best practices for declaring variables and functions </Info> <Warning> **Prerequisites:** This guide builds on your understanding of [Scope and Closures](/concepts/scope-and-closures) and the [Call Stack](/concepts/call-stack). If you're not comfortable with how JavaScript manages scope, read those guides first. </Warning>Hoisting is JavaScript's behavior of moving declarations to the top of their scope during the compilation phase, before any code is executed. When JavaScript prepares to run your code, it first scans for all variable and function declarations and "hoists" them to the top of their containing scope. Only the declarations are hoisted, not the initializations. According to the ECMAScript specification, variable declarations are instantiated when their containing environment record is created, which is why they appear to "move" to the top.
Here's the key insight: hoisting isn't actually moving your code around. It's about when JavaScript becomes aware of your variables and functions during its two-phase execution process.
<Note> The term "hoisting" doesn't appear in the ECMAScript specification. It's a conceptual model that describes the observable behavior of how JavaScript handles declarations during compilation. </Note>Imagine you're moving into a new apartment. Before you even show up with your boxes, the moving company has already:
When you arrive, you know where everything will go, but the actual furniture (the values) hasn't been unpacked yet.
┌─────────────────────────────────────────────────────────────────────────┐
│ HOISTING: THE MOVING DAY ANALOGY │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ BEFORE YOU ARRIVE (Compilation Phase) │
│ ───────────────────────────────────── │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ LIVING ROOM │ │ BEDROOM │ │ KITCHEN │ │
│ │ │ │ │ │ │ │
│ │ [empty] │ │ [empty] │ │ [empty] │ │
│ │ │ │ │ │ │ │
│ │ Reserved │ │ Reserved │ │ Reserved │ │
│ │ for: sofa │ │ for: bed │ │ for: table │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ AFTER UNPACKING (Execution Phase) │
│ ───────────────────────────────── │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ LIVING ROOM │ │ BEDROOM │ │ KITCHEN │ │
│ │ │ │ │ │ │ │
│ │ [SOFA] │ │ [BED] │ │ [TABLE] │ │
│ │ │ │ │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ JavaScript knows about all variables before execution, but their │
│ values are only assigned when the code actually runs. │
│ │
└─────────────────────────────────────────────────────────────────────────┘
This is exactly how hoisting works:
Not all declarations are hoisted the same way. Understanding these differences is crucial:
| Declaration Type | Hoisted? | Initialized? | Accessible Before Declaration? |
|---|---|---|---|
var | Yes | Yes (undefined) | Yes (returns undefined) |
let / const | Yes | No (TDZ) | No (ReferenceError) |
| Function Declaration | Yes | Yes (full function) | Yes (fully usable) |
| Function Expression | Depends on var/let/const | No | No |
class | Yes | No (TDZ) | No (ReferenceError) |
import | Yes | Yes | Yes (but side effects run first) |
Let's explore each one in detail.
Variables declared with var are hoisted to the top of their function (or global scope) and automatically initialized to undefined. As MDN documents, var declarations are processed before any code is executed, which is why accessing a var variable before its declaration returns undefined rather than throwing an error.
console.log(greeting) // undefined (not an error!)
var greeting = "Hello"
console.log(greeting) // "Hello"
When you write code with var, JavaScript essentially transforms it during compilation:
┌─────────────────────────────────────────────────────────────────────────┐
│ var HOISTING TRANSFORMATION │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ YOUR CODE: HOW JAVASCRIPT SEES IT: │
│ ────────── ────────────────────── │
│ │
│ console.log(x); var x; // Hoisted! │
│ var x = 5; console.log(x); // undefined │
│ console.log(x); x = 5; // Assignment │
│ console.log(x); // 5 │
│ │
└─────────────────────────────────────────────────────────────────────────┘
var is function-scoped, meaning it's hoisted to the top of the containing function:
function example() {
console.log(message) // undefined
if (true) {
var message = "Hello"
}
console.log(message) // "Hello"
}
example()
Even though message is declared inside the if block, var ignores block scope and hoists to the function level.
Here's where many developers get confused: let and const are hoisted, but they behave differently from var. They enter what's called the Temporal Dead Zone (TDZ).
// TDZ starts at the beginning of the block
console.log(name) // ReferenceError: Cannot access 'name' before initialization
let name = "Alice"
// TDZ ends here
The Temporal Dead Zone is the period between entering a scope and the actual declaration of a let or const variable. During this time, the variable exists (JavaScript knows about it), but accessing it throws a ReferenceError. Learn more about the TDZ in our dedicated Temporal Dead Zone guide.
┌─────────────────────────────────────────────────────────────────────────┐
│ TEMPORAL DEAD ZONE (TDZ) │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ function example() { │
│ // ┌─────────────────────────────────────────────┐ │
│ // │ TEMPORAL DEAD ZONE FOR 'x' │ │
│ // │ │ │
│ // │ console.log(x); // ReferenceError! │ │
│ // │ console.log(x); // ReferenceError! │ │
│ // │ console.log(x); // ReferenceError! │ │
│ // └─────────────────────────────────────────────┘ │
│ │
│ let x = 10; // ← TDZ ends here, 'x' is now accessible │
│ │
│ console.log(x); // 10 ✓ │
│ } │
│ │
└─────────────────────────────────────────────────────────────────────────┘
The TDZ exists to catch bugs. Consider this code:
let x = "outer"
function example() {
console.log(x) // What should this print?
let x = "inner"
}
Without the TDZ, the console.log(x) might confusingly access the outer x. With the TDZ, JavaScript tells you immediately that something is wrong: you're trying to use a variable before it's ready.
Here's proof that let is actually hoisted (just with TDZ behavior):
let x = "outer"
{
// If 'x' wasn't hoisted, this would print "outer"
// But instead, we get a ReferenceError because the inner 'x' IS hoisted
// and creates a TDZ that shadows the outer 'x'
console.log(x) // ReferenceError: Cannot access 'x' before initialization
let x = "inner"
}
The fact that we get a ReferenceError instead of "outer" proves that the inner let x declaration was hoisted and is "shadowing" the outer x from the start of the block.
Function declarations are fully hoisted. Both the name AND the function body are moved to the top of the scope. This is why you can call a function before its declaration:
// This works perfectly!
sayHello("World") // "Hello, World!"
function sayHello(name) {
console.log(`Hello, ${name}!`)
}
This is a critical distinction:
<Tabs> <Tab title="Function Declaration"> ```javascript // ✓ Works - function declarations are fully hoisted greet() // "Hello!"function greet() {
console.log("Hello!")
}
```
var greet = function() {
console.log("Hello!")
}
```
With `var`, the variable `greet` is hoisted and initialized to `undefined`. Calling `undefined()` throws a TypeError.
const greet = function() {
console.log("Hello!")
}
```
With `let`/`const`, the variable is in the TDZ, so we get a ReferenceError.
Arrow functions are always expressions, so they're never hoisted as functions:
// ✗ ReferenceError
sayHi() // ReferenceError: Cannot access 'sayHi' before initialization
const sayHi = () => {
console.log("Hi!")
}
Classes are hoisted similarly to let and const. They enter the TDZ and cannot be used before their declaration:
// ✗ ReferenceError
const dog = new Animal("Buddy") // ReferenceError: Cannot access 'Animal' before initialization
class Animal {
constructor(name) {
this.name = name
}
}
This applies to both class declarations and class expressions:
// Class declaration - TDZ applies
new MyClass() // ReferenceError
class MyClass {}
// Class expression - follows variable hoisting rules
new MyClass() // ReferenceError (const is in TDZ)
const MyClass = class {}
Import declarations are hoisted to the very top of their module. However, the imported module's code runs before your module's code:
// This works even though the import is "below"
console.log(helper()) // Works!
import { helper } from './utils.js'
What happens when a variable and a function have the same name? There's a specific order of precedence:
console.log(typeof myName) // "function"
var myName = "Alice"
function myName() {
return "I'm a function!"
}
console.log(typeof myName) // "string"
Here's what happens:
var myName and function myName are hoistedfunction myName overwrites the undefined from var myNamevar myName = "Alice", it reassigns to a stringMultiple var declarations of the same variable are merged into one:
var x = 1
var x = 2
var x = 3
console.log(x) // 3
This is essentially the same as:
var x
x = 1
x = 2
x = 3
let x = 1
let x = 2 // SyntaxError: Identifier 'x' has already been declared
The most common hoisting trap involves function expressions with var:
// What does this print?
console.log(sum(2, 3))
var sum = function(a, b) {
return a + b
}
Answer: TypeError: sum is not a function
┌─────────────────────────────────────────────────────────────────────────┐
│ THE FUNCTION EXPRESSION TRAP │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ YOUR CODE: HOW JAVASCRIPT SEES IT: │
│ ────────── ────────────────────── │
│ │
│ console.log(sum(2, 3)) var sum; // undefined │
│ console.log(sum(2, 3)) // Error! │
│ var sum = function(a, b) { sum = function(a, b) { │
│ return a + b return a + b │
│ } } │
│ │
│ When sum(2, 3) is called, sum is undefined. │
│ Calling undefined(2, 3) throws TypeError! │
│ │
└─────────────────────────────────────────────────────────────────────────┘
If you need to call a function before its definition, use a function declaration:
// ✓ This works
console.log(sum(2, 3)) // 5
function sum(a, b) {
return a + b
}
Or, declare your function expressions at the top:
// ✓ Define first, use later
const sum = function(a, b) {
return a + b
}
console.log(sum(2, 3)) // 5
You might wonder why JavaScript has this seemingly confusing behavior. There are historical and practical reasons:
Hoisting enables functions to call each other regardless of declaration order:
function isEven(n) {
if (n === 0) return true
return isOdd(n - 1) // Can call isOdd before it's defined
}
function isOdd(n) {
if (n === 0) return false
return isEven(n - 1) // Can call isEven
}
console.log(isEven(4)) // true
console.log(isOdd(3)) // true
Without hoisting, you'd need to carefully order all function declarations or use forward declarations like in C.
JavaScript engines process code in two phases:
<Steps> <Step title="Compilation Phase"> The engine scans the code and registers all declarations in memory. Variables are created but not assigned values (except functions, which are fully created). </Step> <Step title="Execution Phase"> The engine runs the code line by line, assigning values to variables and executing statements. </Step> </Steps>This two-phase approach is why hoisting exists. It's a natural consequence of how JavaScript is parsed and executed.
```javascript
function processUser(user) {
// All declarations at the top
const name = user.name
const email = user.email
let isValid = false
// Logic follows
if (name && email) {
isValid = true
}
return isValid
}
```
```javascript
// ✓ Good
const API_URL = 'https://api.example.com'
let currentUser = null
// ✗ Avoid
var counter = 0
```
`const` and `let` have more predictable scoping and the TDZ catches bugs early. The State of JS 2023 survey shows that over 93% of developers now regularly use `const` and `let`, reflecting a strong industry shift away from `var`.
```javascript
// ✓ Clear intent, fully hoisted
function calculateTotal(items) {
return items.reduce((sum, item) => sum + item.price, 0)
}
// ✓ Also fine - but define before use
const calculateTax = (amount) => amount * 0.1
```
```javascript
// ✓ Good - imports at top
import { useState, useEffect } from 'react'
import { fetchUser } from './api'
function UserProfile() {
// Component code
}
```
```javascript
// ✗ Bad - confusing, relies on hoisting
function bad() {
console.log(x) // undefined - works but confusing
var x = 5
}
// ✓ Good - clear and predictable
function good() {
const x = 5
console.log(x) // 5
}
```
Hoisting is declaration movement — JavaScript moves declarations to the top of their scope during compilation, but assignments stay in place
var is hoisted and initialized to undefined — You can access it before declaration, but the value is undefined
let and const are hoisted into the TDZ — They exist but throw ReferenceError if accessed before declaration
Function declarations are fully hoisted — Both the name and body are available before the declaration appears in code
Function expressions follow variable rules — A var function expression gives TypeError, a let/const expression gives ReferenceError
Classes are hoisted with TDZ — Like let/const, classes cannot be used before declaration
Imports are hoisted to the top — But module side effects execute before your code runs
Functions beat variables — When a function and var share a name, the function takes precedence initially
TDZ exists to catch bugs — It prevents confusing behavior where inner variables might accidentally use outer values
Best practice: declare at the top — Don't rely on hoisting for readability; put declarations where you use them
</Info>**Answer:**
```
undefined
10
```
The `var x` declaration is hoisted and initialized to `undefined`. The first `console.log` prints `undefined`. Then `x` is assigned `10`, and the second `console.log` prints `10`.
**Answer:**
```
ReferenceError: Cannot access 'y' before initialization
```
`let` is hoisted but enters the Temporal Dead Zone. Accessing it before declaration throws a `ReferenceError`.
var sayHi = function() {
console.log("Hi!")
}
```
**Answer:**
```
TypeError: sayHi is not a function
```
The `var sayHi` is hoisted and initialized to `undefined`. Calling `undefined()` throws a `TypeError`.
function sayHello() {
console.log("Hello!")
}
```
**Answer:**
```
Hello!
```
Function declarations are fully hoisted. The entire function is available before its declaration in the code.
**Answer:**
```
number
```
Both are hoisted, with the function declaration winning initially. But then `var a = 1` executes and reassigns `a` to the number `1`. So `typeof a` is `"number"`.
function test() {
console.log(x)
const x = "inner"
}
test()
```
**Answer:**
This throws `ReferenceError: Cannot access 'x' before initialization`.
Even though there's an outer `x`, the inner `const x` is hoisted within `test()` and creates a TDZ. The inner `x` shadows the outer `x` from the start of the function, so the `console.log(x)` tries to access the inner `x` which is still in the TDZ.
This proves that `const` (and `let`) ARE hoisted; they just can't be accessed until initialized.