Back to Blog
Javascript

Hoisting Explained

Before your code runs, JavaScript moves certain declarations to the top of their scope

Wajahat Zia
Wajahat Zia
8/27/2025
0 views

Hoisting is one of those JavaScript behaviors that happens automatically, often without us realizing it.

In simple terms: before your code runs, JavaScript moves certain declarations to the top of their scope (a scope can be global, function, or block).

That’s why you can sometimes use a function or variable before it’s written in the code.

Declarations are any declarations of functions, variables, classes or imports.

Types of Hoisting

Value Hoisting

Use a variable’s value before the code line where the variable is declared in the scope

Function Declarations → Fully hoisted (you can call them before they appear).

Declaration Hoisting

Be able to reference a variable before the code line where the variable is declared, although its value isn’t hoisted and remains undefined until the code line is executed in the scope

var Variables → Declaration hoisted, value set to undefined until assignment.

Declaration Hoisted, Value not assigned

The scope is aware of the variables existence but the value is not hoisted

let / const Variables & Classes → Declaration hoisted, but not initialized (they live in the temporal dead zone until their line runs).

Declaration causes the scope to behave different

There is a difference in how the scope works before and after the declaration (i.e. the whole scope needs to work in sync because of the declaration)

Imports → Always hoisted to the top of the module before code runs.

Value Hoisting

From the 4 different types of function we can define (Function Declarations, Function Expressions, IIFE and Arrow Functions), only Function Declarations are hoisted as per Value Hoisting

javascript
console.log(foo("cat")) // "cat"
console.log(bar("dog") // ReferenceError: Cannot access 'bar' before initialization
// Function Declaration
function foo(a) {
return a
}
// Function Expressions
const bar = function (a) {
return n
}
// Immediately Invoked Function Expressions (IIFE)
// This function is executed where its declared in the code
(function (a) {
return a
})("fizz")
javascript
console.log(buzz("cat")) // ReferenceError: Cannot access 'buzz' before initialization
// Arrow Function
const buzz = (a) => {
return a
}

So, remember:

✅ Function Declarations → hoisted
❌ Function Expressions / Arrow Functions → not hoisted

Declaration Hoisting

javascript
console.log(a) // undefined
var a = "some value"
console.log(a) // "some value"
function foo() {
console.log(a) // undefined
var a = "scoped value"
console.log(a) // "scoped value"
}
foo()
Comments illustrate the output on terminal

The above example is interpreted as:

javascript
var a
console.log(a) // undefined
a = "some value"
console.log(a) // "some value"
function foo() {
var a
console.log(a) // undefined
a = "scoped value"
console.log(a) // "scoped value"
}
foo()

As illustrated in the codes above, var declarations are hoisted up before execution in the scope. This means if you access a variable before its declared, the value of that variable is always undefined (only its declaration and default initialization undefined is hoisted, not its value assignment)

A point to note here is that although the global scope does have a assigned a value, a is still undefined in the function scope because the local declaration of a inside the function scope takes precedence over (shadows) global scope (<link to understanding closures here>). If the function scope hadn’t redeclared a , a would not have been undefined

javascript
console.log(a) // undefined
var a = "some value"
console.log(a) // "some value"
var b = "once"
function foo() {
console.log(a) // undefined
console.log(b) // "once" <- not undefined as the function scope does not redeclare b
var a = "scoped value"
console.log(a) // "scoped value"
}
foo()

For all the above reasons, as a best practice to follow, we should always try to place var variables near to the top of the function as possible (or the top of the file depending on the scope). More importantly, avoid var in modern code and use let and const for predictable scoping

Declaration Hoisted, Value not assigned

Unlike var, variables declared with let and const are not immediately initialized. Some also consider these as non-hoisting because we run into a concept called Temporal dead zone as accessing values of let , const and classes declarations before their declaration code line is strictly forbidden.

javascript
const a = "foo"
console.log(a) // "foo"
console.log(b) // ReferenceError
const b = "bar"

Although considered non-hoisting, the below code snippet suggests some for of hoisting does occur as declaring x in the inner scope causes console.log(x) to produce a ReferenceError whereas it should be able to read the value of x from the outer scope. Inner scope declaration taints the outer scope declaration due to hoisting

javascript
const x = "foo"
{
console.log(x) // ReferenceError
const x = "bar"
}

Another point to consider is that the below code snippet is not a form of hoisting

javascript
{
var a = "foo";
}
console.log(a) // "foo"

Simply because var declarations are not scoped to blocks, var a is hoisted to the top of the outer scope, a = "foo" is executed from the inner block and then the initialized a is available to console.log . (<read more about scopes here>)

Declaration causes the scope to behave different

Import declarations are hoisted to the top of the module regardless of where they are declared. The values of these import declarations are available even before the place that declares them and the imported module’s side effects are produced before the rest of the module code starts executing.

javascript
const foo = new Foo()
foo.create()
import { Foo } from './modules/foo.js'
foo.createBar()

Interpreter would rearrange this as

javascript
// Step 1: Module loading phase
// All static imports are resolved *before* any code runs.
import { Foo } from './modules/foo.js'
// Step 2: Execution phase
const foo = new Foo()
foo.create()
foo.createBar()

A point to remember is that we are talking about Static Imports. Another type of import is called Dynamic Imports and these declarations act more like a function-like expression that returns a Promise . Dynamic Imports follow the rules of let , const or var accordingly to their declaration type.

javascript
// Static Import
import { Foo } from './modules/foo.js'
function bar() {
// Dynamic Import - Not hoisted as static import do
const bImport = await import("b").then(x => x)
}

Summary

Functions → Only function declarations are hoisted.
var → Declaration hoisted, initialized with undefined.
let, const, classes → Hoisted but uninitialized (TDZ).
Imports → Always hoisted (static imports run before any code).

👉 As a rule of thumb:

Write your variables and imports at the top, and use let/const over var. This avoids confusion and keeps code predictable.

FundamentalsJavascript