JavaScript is known for being quirky and flexible—traits that can make it both exciting and frustrating to learn. One of the most misunderstood features of the language is hoisting. It confuses many developers, especially beginners, because it doesn’t behave like you’d expect based on how code is written.
In this article, we’ll unpack JavaScript hoisting in the simplest way possible. We’ll walk through what hoisting really is, how it works behind the scenes, and why understanding it is essential to becoming a better JavaScript developer.
JavaScript Promises: How They Work and Why They Matter
What is Hoisting in JavaScript?
Hoisting is a JavaScript mechanism where variable and function declarations are moved to the top of their scope (either global or function scope) before the code is executed.
Let’s be clear: only declarations are hoisted—not initializations or assignments.
This means you can use certain variables and functions before they are declared in the code. Here’s a quick example:
console.log(greeting); // undefined
var greeting = "Hello!";
You might expect this to throw an error because greeting
is used before it is declared. But instead, JavaScript prints undefined
. That’s hoisting in action.
Behind the Scenes: What Really Happens?
When the JavaScript engine compiles your code, it does two things:
- Creates memory space for variables and functions.
- Executes the code line by line.
During the first phase (also called the creation phase), JavaScript scans your code and hoists all variable and function declarations to the top of their scope. Only declarations, not initializations.
The previous example is interpreted by the engine like this:
var greeting;
console.log(greeting); // undefined
greeting = "Hello!";
Understanding this mental model helps you avoid strange bugs and surprises.
Function Hoisting
Functions in JavaScript are hoisted differently than variables. Specifically, function declarations are hoisted with their entire definition. This means you can call a function before you define it in your code.
sayHello(); // "Hello, world!"
function sayHello() {
console.log("Hello, world!");
}
This works perfectly fine because the function declaration sayHello()
is hoisted along with its implementation.
Function Expressions Are Not Hoisted the Same Way
Let’s look at this example:
sayHi(); // TypeError: sayHi is not a function
var sayHi = function() {
console.log("Hi!");
};
Why does this throw an error?
Because sayHi
is a variable, and only the declaration var sayHi
is hoisted. The function assignment isn’t. So this is how the engine sees it:
var sayHi;
sayHi(); // TypeError
sayHi = function() {
console.log("Hi!");
};
At the time of the function call, sayHi
is just a variable with the value undefined
.
Variable Hoisting: var
, let
, and const
var
is Fully Hoisted (But Only Declared)
When you use var
, the variable is hoisted and initialized with undefined
.
console.log(score); // undefined
var score = 100;
Equivalent to:
var score;
console.log(score); // undefined
score = 100;
This can lead to bugs if you’re not careful, especially in large codebases or when using the same variable name in multiple scopes.
let
and const
Are Hoisted Too (But in a Different Way)
Contrary to popular belief, let
and const
are also hoisted. However, they are not initialized during the hoisting phase. Instead, they remain in a “temporal dead zone” (TDZ) from the start of the block until the declaration is encountered.
console.log(name); // ReferenceError
let name = "Alice";
This behavior is intentional—it prevents you from accessing variables before they are safely initialized.
Understanding the Temporal Dead Zone (TDZ)
The Temporal Dead Zone is the period between the start of a block scope and when a variable is declared using let
or const
.
Accessing the variable during this time results in a ReferenceError, not undefined
.
{
// TDZ starts here
console.log(age); // ReferenceError
let age = 30;
}
This rule encourages safer, more predictable code compared to var
.
Real-World Examples Where Hoisting Can Hurt
Example 1: Accidental Overwrites
var value = 10;
function test() {
console.log(value); // undefined
var value = 20;
}
test();
You might think this would print 10
because value
is declared outside. But inside the function, var value
is hoisted to the top of the function scope, so you’re logging the local (and still undefined) variable.
Rewritten by the JavaScript engine:
function test() {
var value;
console.log(value); // undefined
value = 20;
}
Example 2: Misplaced Function Expressions
runTask(); // TypeError
var runTask = function() {
console.log("Running...");
};
This causes confusion because developers often expect function-like behavior. But since this is a function expression, it behaves like a variable and only the declaration is hoisted.
Best Practices to Avoid Hoisting Confusion
1. Use let
and const
Instead of var
Modern JavaScript development discourages the use of var
. Stick with let
and const
for block-scoped and more predictable behavior.
// Bad
var count = 5;
// Good
let count = 5;
const PI = 3.14;
2. Always Declare Variables at the Top of Their Scope
While hoisting allows you to declare variables anywhere, it’s a good habit to declare them at the beginning of the function or block to improve readability.
3. Don’t Use Functions Before Declaring Them (Even if You Can)
Just because you can call a function before it’s defined doesn’t mean you should. Declaring functions before usage leads to cleaner and more predictable code.
4. Avoid Re-declaring Variables
Especially with var
, re-declarations can happen silently, leading to unpredictable bugs.
var a = 10;
var a = 20; // No error, but risky
let b = 10;
let b = 20; // SyntaxError
Deep Dive: Hoisting in Function vs Block Scope
Hoisting behaves differently depending on the scope.
Function Scope
In a function, var
declarations are hoisted to the top of the function:
function demo() {
console.log(num); // undefined
var num = 5;
}
Equivalent to:
function demo() {
var num;
console.log(num); // undefined
num = 5;
}
Block Scope
Variables declared with let
or const
inside a block are not accessible outside and follow the TDZ rule:
{
console.log(item); // ReferenceError
let item = "Apple";
}
Visualizing Hoisting
A helpful way to internalize hoisting is to imagine the compiler reordering your code:
// What you write
function show() {
console.log(msg);
var msg = "Hello!";
}
JavaScript engine transforms it like this:
function show() {
var msg;
console.log(msg); // undefined
msg = "Hello!";
}
Once you start visualizing this internally, hoisting becomes much easier to grasp and predict.
Summary of Key Points
Concept | Behavior |
---|---|
var | Hoisted and initialized to undefined |
let and const | Hoisted but not initialized (TDZ) |
Function Declarations | Fully hoisted with definition |
Function Expressions | Only variable is hoisted |
TDZ | Period when let /const exists but is not accessible |
Best Practice | Use let /const , declare before use, avoid var |
Conclusion
Hoisting in JavaScript is one of those tricky concepts that becomes surprisingly simple once you understand how the language processes your code. The key takeaway is this: declarations are hoisted, not initializations.
By understanding how and why hoisting works, you can write cleaner, safer, and more predictable code. It helps eliminate subtle bugs, makes debugging easier, and improves code readability.
Whether you’re just starting out or revisiting JavaScript after a while, mastering hoisting is a vital step toward writing professional, error-free JavaScript.
Let me know if you’d like this article in PDF, DOCX format, or need help publishing it to a blog or portfolio!