๐ญ JavaScript Hoisting: The Invisible Code Reorganization You Need to Know


๐ค Take a look at this JavaScript code. What do you think it outputs?
console.log(myVar); // What happens here?
console.log(myFunc()); // And here?
var myVar = "Hello World";
function myFunc() {
return "I'm a function!";
}
If you expected errors, you might be surprised. This code actually runs without throwing any errors! The first console.log
prints undefined
, and the second prints "I'm a function!"
. Welcome to the mysterious world of JavaScript hoisting.
๐ What is Hoisting?
Javascript moves the variables and function declarations to the top of their containing scope during the compilation phase. The behavior exhibited by Javascript is known an Hoisting. Itโs as if, the Javascript engine moves your declarations physically upward in the code you write, although thatโs not what exactly happens under the hood.
Or visualize it this way: before JavaScript executes your code line by line, it takes a first pass to scan for all declarations and allocates memory for them. We have covered this concept in our previous blog where we talked about the Execution Context and itโs two distinct phases. You can find the link to the blog here.
๐งช Variable Hoisting Deep Dive
The var
Keyword
When you declare a variable with var
, JavaScript hoists the declaration but not the initialization. Here's what really happens:
// The code you write:
console.log(name); // undefined
var name = "JavaScript";
console.log(name); // "JavaScript"
// How JavaScript interprets it:
var name; // Declaration is hoisted
console.log(name); // undefined (declared but not assigned)
name = "JavaScript"; // Assignment stays in place
console.log(name); // "JavaScript"
This is why var
variables return undefined
instead of throwing a ReferenceError when accessed before declaration. The variable exists in memory but hasn't been assigned a value yet.
var
declarations are function-scoped, meaning they're hoisted to the top of the nearest function or global scope:
function example() {
console.log(x); // undefined (not ReferenceError)
if (true) {
var x = 1;
}
console.log(x); // 1
}
// JavaScript interprets this as:
function example() {
var x; // Hoisted to function top
console.log(x); // undefined
if (true) {
x = 1; // Assignment stays here
}
console.log(x); // 1
}
let
and const
Behavior
Here's where things get interesting. let
and const
are technically hoisted too, but they behave very differently:
console.log(myLet); // ReferenceError: Cannot access 'myLet' before initialization
let myLet = "Hello";
console.log(myConst); // ReferenceError: Cannot access 'myConst' before initialization
const myConst = "World";
The key difference is the Temporal Dead Zone (TDZ). While let
and const
declarations are hoisted, they remain uninitialized until the JavaScript engine encounters their declaration in the code. Trying to access them before that point throws a ReferenceError.
function demonstrateTDZ() {
// TDZ starts here for 'temp'
console.log(temp); // ReferenceError
let temp = "Here we go!"; // TDZ ends here
console.log(temp); // "Here we go!"
}
Unlike var
, let
and const
are block-scoped:
function blockScopeExample() {
console.log(x); // ReferenceError
if (true) {
let x = 1; // Only exists within this block
}
console.log(x); // ReferenceError
}
๐ ๏ธ Function Hoisting
Function Declarations
Function declarations are completely hoisted, meaning both the function name and its implementation are moved to the top of the scope:
// This works perfectly:
greetings(); // "Hello, World!"
function greetings() {
console.log("Hello, World!");
}
// JavaScript treats it as:
function greetings() {
console.log("Hello, World!");
}
greetings(); // "Hello, World!"
This complete hoisting allows you to organize your code with the main logic at the top and helper functions at the bottom, improving readability.
Function Expressions and Arrow Functions
Function expressions and arrow functions follow variable hoisting rules and not function hoisting rules:
// This won't work:
sayHi(); // TypeError: sayHi is not a function
var sayHi = function() {
console.log("Hi!");
};
// JavaScript interprets this as:
var sayHi; // undefined
sayHi(); // TypeError: sayHi is not a function
sayHi = function() {
console.log("Hi!");
};
The same applies to arrow functions:
greet(); // ReferenceError: Cannot access 'greet' before initialization
const greet = () => {
console.log("Greetings!");
};
๐ณ๏ธ Common Hoisting Gotchas
Here are some classic examples that trip up developers:
Pitfall 1: Variable Shadowing
var x = 1;
function test() {
console.log(x); // What prints here?
var x = 2;
console.log(x); // And here?
}
test();
Answer: The first console.log
prints undefined
, not 1
. The local var x
declaration is hoisted, shadowing the global x
.
Pitfall 2: Function vs Variable Hoisting
console.log(foo); // What happens?
var foo = "variable";
function foo() {
return "function";
}
console.log(foo); // What about here?
Answer: The first console.log
prints the function (functions are hoisted completely), while the second prints "variable"
(the assignment overwrites the function). To understand this better, look at the code below to visualize how javascript treats the above piece of code as.
// Since this function is hoisted completely
function foo() {
return "function";
}
console.log(foo); // prints function
var foo = "variable"; // here it's overwritten
console.log(foo); // prints variable
Pitfall 3: Loop Confusion
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
This prints 3
three times because var
is function-scoped, and by the time the timeouts execute, the loop has finished and i
is 3
.
๐ต๏ธ Real-World Debugging
Hoisting can cause subtle bugs that are hard to track down:
function processUser(user) {
if (user.isActive) {
var userData = getUserData(user.id);
// ... process userData
}
console.log(userData); // undefined, not ReferenceError
// This might cause issues downstream
}
The userData
variable is hoisted to the function top, so it exists even when the if
condition is false, potentially masking bugs.
๐ง Understanding the Why
Hoisting exists for historical reasons. JavaScript was designed to be forgiving and allow developers to write code in a more natural, top-down manner. Function hoisting, in particular, enables mutual recursion and better code organization:
function isEven(n) {
return n === 0 || isOdd(n - 1);
}
function isOdd(n) {
return n !== 0 && isEven(n - 1);
}
Without hoisting, this mutual recursion wouldn't be possible with function declarations.
๐ฏ Conclusion
Hoisting is one of JavaScript's most misunderstood features, but understanding it is crucial for writing reliable code and debugging issues effectively. Here are the key takeaways:
var
declarations are hoisted and initialized asundefined
.let
andconst
are hoisted but remain in the Temporal Dead Zone until declaration.Function declarations are completely hoisted, function expressions follow variable rules.
Modern best practices favor
const
/let
overvar
and explicit declaration order.Understanding hoisting helps you avoid common pitfalls and write more predictable code.
The next time you encounter unexpected undefined
values or mysterious ReferenceErrors, remember to think about hoisting. It might just be the invisible code reorganization causing the confusion!
โ๏ธ Missed My Last Blog?
Read it here.
๐ Get clarity on how JavaScript prepares to run your code, what really happens during the creation and execution phases, and how the call stack handles function calls behind the scenes.
๐ค Connect with Me
If you enjoyed this blog and want to stay updated with more JavaScript insights, developer tips, and tech deep-dives โ feel free to connect with me across platforms:
๐ LinkedIn: Connect with me on LinkedIn
๐ Medium: Follow me on Medium
๐ Hashnode: Check out my Hashnode blog
I appreciate your support and look forward to connecting with fellow devs, learners, and curious minds like you! ๐
Subscribe to my newsletter
Read articles from K Manoj directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

K Manoj
K Manoj
Backend Web Developer | Security Enthusiast |