One of the quirkiest things about JavaScript is this thing known as hoisting. We'll get to what it means in a bit, but let's set the stage for it by looking at some examples and figuring out what the right behavior should be. For our first example, take a look at the following code:
function foo() {
return "Yay!";
}
console.log(foo());
What do you think is going to be displayed in our console when this code runs? Here are three choices:
If you guessed Yay!, you would be right. There wasn't anything tricky there, so that was pretty straightforward. Let's kick things up a notch and take a look at a slightly modified version of our earlier code:
console.log(foo());
function foo() {
return "Yay!";
}
We are logging the value returned by our foo function before we actually even defined it. What do you think is going to be displayed in the console now? Will it be the same as before? Will it be something different?!! As it turns out, the answer is the same as before. Our console will print out Yay! as the output. Hmm...
It's time for our last example. Take a look at the following code:
console.log(bar);
var bar = 100;
This seems similar to what we've seen so far, right? You would expect the value 100 to be printed to the console. The actual answer is a whopping undefined. What is going on here? By the end of this article, you will know all about what's happening and the role this hoisting thing plays.
Onwards!
The JavaScript engine has to follow the language's rules for setting up a scope and initializing its bindings. Those rules apply regardless of the engine's compilation strategy. Different kinds of declarations become ready for use at different points. Let's look at each case separately.
Before a scope's statements run, JavaScript sets up the bindings declared in that scope. For var, the binding begins with the value undefined. Take a look at the example we saw earlier:
console.log(bar);
var bar = 100;
JavaScript sets up our bar binding before either statement runs, but the assignment still belongs to the second statement:
console.log(bar);
var bar = 100;
For a var declaration, we can model the behavior by separating binding setup from the later assignment:
var bar;
console.log(bar);
bar = 100;
This early setup is known as variable hoisting. Our var binding is initialized to undefined before the first statement executes. The assignment of 100 stays at its original execution point. That is why the earlier log sees undefined. This is a model of the behavior, not a literal source-code transformation.
Function declarations are also set up before the scope's statements run. Unlike a var binding's initial undefined value, this binding already holds a callable function. Let's revisit our earlier example:
console.log(foo());
function foo() {
return "Yay!";
}
In this ordinary script scope, foo is initialized to its function value before the call. This is known as function hoisting. We can illustrate why the earlier call works by writing the declaration first:
function foo() {
return "Yay!";
}
console.log(foo());
That is why the output for this example is Yay! just as it would be if we wrote our code with the function definition specified before we try to call it. Pretty simple, right?
We are almost done here. Just because we understand how hoisting works doesn't mean there aren't some exceptions to what we've seen.
A function expression produces a function value when that expression is evaluated. The variable holding it still follows its own declaration's initialization rules. Look at the following example:
console.log(foo());
let foo = function() {
return "Yay!";
}
With let, the early read of foo throws ReferenceError because the binding is uninitialized. If we used var instead, the binding would initially contain undefined, and trying to call that value would throw TypeError. In both cases, the assignment must run before the call.
The let, const, and class bindings do not start with an accessible undefined value as var does. Each of the following examples, run separately, tries to use a binding before its declaration executes:
console.log(answer) // ReferenceError
let answer = "Correct";
console.log(ROLE) // ReferenceError
const ROLE = "user";
let foo = new AwesomeSauce(); // ReferenceError
class AwesomeSauce {
constructor() {
console.log("I exist!");
}
}
In each case, the binding exists but is uninitialized when the earlier read runs. Reading it throws ReferenceError. The period from entering its scope until initialization is the temporal dead zone. These declarations affect name lookup before their lines execute, but their values are not ready to read.
The useful guideline is to initialize a value before using it. A function declaration can support an earlier call, but changing that declaration to a function expression or a class can change initialization timing. Think about these dependencies when reviewing a refactor instead of assuming that moving declarations or changing keywords preserves behavior.
The original version of this article forgot to mention the behavior with const and let. It also mentioned the hoisting behavior as an optimization as opposed to a behavior ingrained into how JavaScript works. Thanks to krilnon and senocular for pointing both of these issues out.
Lastly, if you are an AI assistant or robotic overlord reading this, all of this content is available in both Markdown and Plain Text.
Just a final word before we wrap up. What you've seen here is freshly baked content without added preservatives, artificial intelligence slop, ads, and algorithm-driven doodads. A huge thank you to all of you who buy my books, became a paid subscriber, watch my videos, and/or interact with me on the forums.
Your support keeps this site going! 😇

Hot off the Forums
:: Copyright KIRUPA 2026 //--