Pre-loader

Javascript Hoisting Explained Simply

Javascript Hoisting Explained Simply

Javascript Hoisting Explained Simply

A few months ago, I was reviewing a PR from a junior dev on our team. The code mostly looked fine, but one log statement caught my eye. A variable was being logged before it was declared, and the dev had added a comment saying, “Works because of hoisting.” The output was undefined, tests were passing, and nobody complained — but the reasoning was shaky. I left a review comment, not to nitpick, but because this exact misunderstanding shows up later as real bugs.

Hoisting is one of those JavaScript concepts people think they understand until they actually have to explain it. Or debug it. Or answer a follow-up question in an interview. Then suddenly everything becomes fuzzy.

The problem isn’t that hoisting is complex. The problem is that it’s usually explained wrong.

Most of us were taught some version of “JavaScript moves declarations to the top.” That sentence sounds helpful at first, but it quietly builds the wrong mental model. JavaScript doesn’t move your code. The engine isn’t rearranging lines before running them. What’s really happening is more structured, more predictable, and honestly easier to reason about once you see it clearly.

Let’s break it down the right way.

The mental model you actually need

If you remember one thing from this article, let it be this: hoisting is a side effect of how the JavaScript engine creates execution contexts.

Every time JavaScript runs code, it does so inside an execution context. There’s a global execution context for your main file, and a new function execution context every time a function is called. Each execution context is created in two phases.

First is the memory creation phase. Second is the execution phase.

This is the part people miss.

During the memory creation phase, JavaScript scans the code and sets up memory for variables and functions in that scope. It decides what identifiers exist and what their initial values should be. Nothing runs yet. No assignments. No logs. Just setup.

Then, in the execution phase, JavaScript actually runs your code line by line.

Hoisting is not a special feature layered on top of this. It’s simply what we observe when something exists in memory before the execution phase reaches its line.

how JavaScript hoisting really works

Here’s what’s actually happening inside the JavaScript engine when your code starts.

When the global scope loads, the engine creates the global execution context. During its memory creation phase, it looks at all declarations in that scope.

Function declarations are fully initialized. The function name points directly to the function object in memory.

Variables declared with var are allocated memory and initialized with the value undefined.

Variables declared with let and const are also allocated memory, but they are not initialized yet.

That’s it. No code movement. No reordering.

When execution starts, JavaScript runs the file top to bottom. If you try to access something that already exists in memory, you’ll get whatever value it currently has. If you try to access something that exists but hasn’t been initialized yet, you’ll get an error.

This explains almost every hoisting behavior people struggle with.

undefined is not an accident

One of the most common debugging moments goes like this: you log a variable before its declaration and see undefined. People often say, “Oh, hoisting.” But they stop there, which is where confusion starts.

You’re not seeing undefined because JavaScript is being lenient. You’re seeing it because var variables are initialized to undefined during the memory creation phase.

That means the variable exists. It’s real. It’s just not assigned yet.

So when execution reaches that console.log, JavaScript finds the variable in memory and reads its current value. At that point, the value is still undefined.

This is very different from a variable that doesn’t exist at all.

ReferenceError vs undefined

Understanding the difference between undefined and ReferenceError is the fastest way to understand hoisting.

If you access a variable and get undefined, it means the identifier exists in the current execution context and has been initialized with the value undefined.

If you access a variable and get a ReferenceError, it means one of two things: either the variable doesn’t exist in the current scope, or it exists but hasn’t been initialized yet.

That second case is what leads us to let, const, and the temporal dead zone.

hoisting with let and const explained

A lot of developers think let and const are “not hoisted.” That’s not accurate.

They are hoisted, but differently.

When the engine goes through the memory creation phase, it registers let and const variables in the execution context. They exist. The engine knows about them. But it does not initialize them.

The period between the start of the scope and the actual declaration is called the temporal dead zone. During this time, accessing the variable throws a ReferenceError.

This is intentional. It prevents you from using variables before they’re ready, which avoids a whole class of bugs that var allows.

Once execution reaches the declaration line, the variable is initialized. From that point on, you can use it normally.

So no, let and const are not magically immune to hoisting. They just have stricter initialization rules.

function hoisting vs variable hoisting

This is another area where interviews trip people up.

Function declarations are hoisted differently than variables. During the memory creation phase, the engine fully initializes function declarations. That means the function is ready to be called before execution reaches its definition in the file.

That’s why calling a function before it’s written works when it’s a function declaration.

Function expressions behave like variables, because they are variables.

If you assign a function to a var, the variable is initialized to undefined during memory creation. The function itself is only assigned during execution. Calling it before that assignment results in a TypeError, because you’re trying to call undefined.

If the function expression is assigned to let or const, the variable is in the temporal dead zone until its declaration runs. Calling it before that gives a ReferenceError.

Same function. Different behavior. The difference isn’t about functions. It’s about how the variable is created in the execution context.

Once you see that, the rules stop feeling arbitrary.

A short digression about “why this matters”

At this point, some people say, “Okay, I get it, but I just avoid using variables before declaration anyway.”

That’s fair, but hoisting still matters.

It matters when you refactor code and switch from function declarations to function expressions.

It matters when a var inside a function shadows something in the global scope.

It matters when you’re debugging a value that’s unexpectedly undefined instead of throwing an error.

And it definitely matters in interviews, where the follow-up questions are designed to test whether you understand what’s happening under the hood or just memorized outcomes.

Alright, back to hoisting.

Hoisting inside functions works the same way

Everything we’ve discussed applies inside functions too.

When a function is called, JavaScript creates a new function execution context. It runs the same two phases: memory creation and execution.

Variables declared with var inside the function are initialized to undefined at the start of the function execution context.

let and const are registered but uninitialized until their declaration line runs.

Function declarations inside the function are fully available from the start of that function’s execution.

This is why scope-related bugs often feel confusing until you start thinking in terms of execution contexts. Once you do, debugging becomes much more mechanical and less emotional.

Stop thinking “JavaScript moves code”

This is the biggest takeaway.

JavaScript does not move your code to the top. It never has. That explanation is a teaching shortcut that causes more harm than good once you move past basics.

The engine creates memory bindings first, then executes code. Hoisting is simply the visible effect of that process.

When you reason about hoisting this way, things like the temporal dead zone, ReferenceError, and undefined all fit into one consistent model. There’s no need to memorize special cases.

I’ve seen developers with two years of experience struggle with hoisting because they were taught metaphors instead of mechanics. I’ve also seen developers with five years of experience have an “oh wow” moment once execution context finally clicked.

That’s usually the turning point.

Final thought

Hoisting isn’t a trick question and it’s not a JavaScript quirk you tolerate. It’s a consequence of how the language executes code. Once you understand execution context and memory creation, hoisting stops being scary and starts being predictable.

That predictability is what makes you faster in real projects and calmer in interviews.

If you’re still confused by JavaScript fundamentals in real projects or interviews, you don’t have to figure it out alone. Visit Agents Arcade and let’s talk.

Written by:Iqra Majid

Iqra Majid is a full-stack JavaScript developer passionate about building modern web applications and AI-powered solutions. I write to share practical insights, real-world experiences, and lessons learned while building products in the tech space.

Previous Post

No previous post

Next Post

No next post

AI Assistant

Online

Hello! I'm your AI assistant. How can I help you today?

06:53 AM