I still remember the first time I completely broke a production feature because I didn't understand hoisting.
I was refactoring a massive legacy file—one of those 2,000-line "god objects" we all dread touching. I moved a few helper functions to the bottom of the file to clean things up. It seemed harmless. "Code runs top-to-bottom, right?" I thought.
Suddenly, half the module stopped working. But it didn't crash. It just started behaving... weirdly. Variables that should have had values were undefined. Functions I thought were safe to call threw errors. I spent four hours debugging what turned out to be a simple misunderstanding of how the JavaScript engine reads code.
If you have ever tried to log a variable and got undefined instead of a ReferenceError, or tried to call a function only to be told "it is not a function," you have met the ghost in the machine: Hoisting.
This concept is the source of countless interview questions and late-night debugging sessions. But once you see what’s happening under the hood, it stops being magic and starts making perfect sense.
Let’s walk through this together.
What Is Hoisting? (The Simple Explanation)
Hoisting is JavaScript's default behavior of moving declarations to the top of the current scope before the code executes.
Ideally, you expect code to run line-by-line, top-to-bottom. But JavaScript is quirky. Before it runs a single line of your code, it scans the entire file and "memorizes" where your variables and functions are. This means you can sometimes use variables and functions before you have actually written them in your code file.
The Mental Model: The "Script Rehearsal"
To truly understand hoisting, you need to stop thinking of the JavaScript engine as just a runner. Think of it as a Director putting on a play.
Imagine you hand a script to a Director. They don't just start reading line 1 and acting it out immediately. That would be chaos. Actors wouldn't know when to enter.
Instead, the Director does two passes:
- The Rehearsal (Creation Phase): The Director skims through the entire script first. They aren't acting yet. They are just making a list of the cast. "Okay, we have a character named
userScore. We have a character namedlogin()." They write these names on a whiteboard. They don't care what the characters say yet; they just want to know who exists. - The Action (Execution Phase): Now, the Director starts at line 1 and actually runs the scene. When the script says "Call
login()", the Director looks at the whiteboard. Since they wrotelogindown during the rehearsal, they know it exists, and the action proceeds.
Hoisting is that Rehearsal phase. It is the engine allocating memory for your variables before it starts executing the logic.
Step-by-Step Code Execution
Let's look at a classic example that trips up almost everyone.
1console.log(myVar);
2var myVar = 10;
3console.log(myVar);
4What you might expect:
A crash on the first line because myVar hasn't been created yet.
What actually happens:
undefined10
Why? Let's trace exactly what the JavaScript engine does with this code.
Step 1: The Creation Phase (The Rehearsal) Before running any code, the engine scans these three lines.
- It sees
var myVar. - It says, "Aha! I need to reserve space in memory for
myVar." - It initializes
myVarwith a default placeholder value:undefined. - It ignores the
= 10part. That is an assignment, which happens later.
Step 2: The Execution Phase (The Action) Now the code actually runs line-by-line.
- Line 1:
console.log(myVar);The engine looks upmyVar. It finds the memory space we created in Step 1. Currently, it holdsundefined. So, it printsundefined. - Line 2:
myVar = 10;Now the assignment happens. The engine updates the memory slot formyVarfromundefinedto10. - Line 3:
console.log(myVar);The engine looks upmyVaragain. It is now10. So, it prints10.
This split between declaration (making space) and assignment (giving value) is the core mechanism of hoisting.
How It Works Internally
To be a senior developer, you need to speak the language of the engine. We need to talk about Execution Contexts.
When your code runs, JavaScript creates a global Execution Context. This context has two distinct stages in its lifecycle:
1. The Creation Phase
This is where the magic happens. The engine sets up the memory heap.
- For
var: It allocates memory and sets the value toundefined. - For
functiondeclarations: It allocates memory and stores the entire function definition inside it. This is why you can call a function before you define it—the whole thing gets hoisted. - For
letandconst: It allocates memory, but it does not initialize it. It marks the variable as "uninitialized." This creates a safety zone we call the Temporal Dead Zone (TDZ).
2. The Execution Phase
This is where the engine runs your code line by line, assigning values and invoking functions.
The key takeaway here is that "Hoisting" isn't the code physically moving. Your lines of text don't rearrange themselves in the file. It is simply a metaphor for how the memory is set up during the Creation Phase.
Common Mistakes Developers Make
I have done code reviews for years, and these are the hoisting traps that developers fall into constantly.
Mistake 1: The "Function Expression" Trap
Developers often assume all functions are hoisted. They are not. Only Function Declarations are hoisted. Function Expressions are treated like variables.
1// Function Declaration - This works!
2sayHello();
3
4function sayHello() {
5 console.log("Hello!");
6}
7
8// Function Expression - This crashes!
9sayGoodbye(); // Error: sayGoodbye is not a function
10
11var sayGoodbye = function() {
12 console.log("Goodbye!");
13};
14Why it fails:
In the Creation Phase, sayGoodbye is treated like a var variable. It is hoisted and set to undefined.
When you try to call sayGoodbye(), you are effectively trying to do undefined(). JavaScript throws an error because "undefined" is not a function.
Mistake 2: The let and const Confusion
"I heard let and const are not hoisted."
This is technically false. They are hoisted. The engine knows they exist. However, unlike var, they are not initialized with undefined. They are stuck in the Temporal Dead Zone.
1console.log(name); // ReferenceError: Cannot access 'name' before initialization
2let name = "Developer";
3With var, you get undefined. With let, you get a loud, angry error. This is a good thing. It forces you to write cleaner code.
Mistake 3: Hoisting Inside Blocks
This one bites people who are used to older JavaScript. var is not block-scoped; it ignores if blocks and loops.
1if (true) {
2 var secret = "I am visible everywhere";
3}
4console.log(secret); // "I am visible everywhere"
5Because var is hoisted to the top of the function (or global scope), the if block doesn't contain it. This leads to variables leaking into places they shouldn't be, causing bugs that are incredibly hard to track down.
Real-World Use Cases
You might be thinking, "This sounds like bad practice. Should I just avoid hoisting?"
Generally, yes. You should declare variables before you use them. However, understanding hoisting is critical for three real-world scenarios:
1. Organizing Code (Function Hoisting)
Many senior developers (myself included) prefer to put the "main" logic at the top of a file and the helper functions at the bottom.
1// Main Logic - Easy to read immediately
2function initApp() {
3 connectDB();
4 startServer();
5 logStatus();
6}
7
8initApp();
9
10// Implementation Details - Hidden at the bottom
11function connectDB() { ... }
12function startServer() { ... }
13function logStatus() { ... }
14This makes the code more readable. You see what the code does immediately, without scrolling through 50 lines of utility functions first. This is only possible because Function Declarations are fully hoisted.
2. Debugging Legacy Code
If you work in a corporate environment, you will encounter code written in 2013 using var. You will see variables declared at the bottom of functions or inside if statements. If you don't understand hoisting, this code will look completely broken to you.
3. Interview Preparation
Hoisting is a filter. It separates developers who copy-paste code from developers who understand the language. It demonstrates deep knowledge of the compilation step vs. the interpretation step.
The Interview Perspective
When I interview candidates, I almost always ask a hoisting question. I am not trying to trick them; I want to know if they can simulate the engine in their head.
The Standard Question
"What is the difference between var, let, and const regarding hoisting?"
Good Answer:
"All three are hoisted. var is initialized with undefined, so you can access it before declaration (though you shouldn't). let and const are not initialized and stay in the Temporal Dead Zone, throwing an error if you touch them too early."
The "Tricky" Example
This is the one that separates the juniors from the seniors.
1var x = 1;
2
3function test() {
4 console.log(x);
5 var x = 2;
6}
7
8test();
9Ask yourself: What does this log? 1? 2?
The Answer: undefined.
The Explanation:
Inside the test function, there is a var x = 2 declaration.
The engine sees this during the Creation Phase of the function's execution context.
It hoists x to the top of the test function and sets it to undefined.
This "local" x shadows the global x.
So when console.log(x) runs, it sees the local x (which is undefined), not the global x (which is 1).
This specific pattern is the cause of so many "why is my variable undefined?" bugs in production.
TL;DR / Key Takeaways
If you are rushing, here is what you need to lock into your memory:
- Hoisting is not magic. It is the memory allocation step before code execution.
- Function Declarations are fully hoisted. You can use them before you write them.
varis hoisted asundefined. It won't crash, but it won't have your value yet.letandconstare hoisted but inaccessible. They live in the "Temporal Dead Zone" until the code execution hits their line.- Function Expressions are variables. They behave like
var(undefined) orlet/const(error), depending on how you define them.
FAQ
Q: Can I stop hoisting from happening?
A: No, it is built into the language engine. However, using let and const effectively "disables" the bad parts of hoisting by throwing errors instead of silently failing with undefined.
Q: Does hoisting work the same in strict mode ('use strict')?
A: Yes. Hoisting mechanics do not change in strict mode. However, strict mode prevents other bad habits, like accidentally creating global variables.
Q: Why was hoisting invented in the first place? A: It wasn't really "invented" as a feature for us; it was a byproduct of how the JS engine manages memory. However, Brendan Eich (creator of JS) has mentioned that allowing function hoisting was intended to help with mutual recursion (Function A calls B, and B calls A).
Q: Are Classes hoisted?
A: Classes in JavaScript behave like let. They are hoisted, but they stay in the Temporal Dead Zone. You cannot instantiate a class before its definition line.
Conclusion
Hoisting is one of those concepts that feels like a hazing ritual for new JavaScript developers. It’s confusing, counter-intuitive, and invisible.
But once you grasp the Creation Phase vs. Execution Phase mental model, the fog clears. You stop guessing why a variable is undefined. You stop fearing the order of your functions. You start writing code that works with the engine, not against it.
My advice? Stick to const and let. Declare your variables at the top of their scope. But when you inevitably dive into a legacy codebase or walk into a technical interview, keep that "Director's Rehearsal" analogy in your back pocket. It might just save your day.
