JavaScript Primer: Hoisting

Exploring the mental model that demystifies the creation phase

Zach Morgan
7 min readNov 15, 2022
Photo by Jonathan Ouimet on Unsplash

A programming language can be either a massively productive tool, or a major source of headaches and frustration. Which it will be depends on how well you understand the rules and nuances of the language. It is completely possible to build multiple projects in JavaScript without having a grasp on a topic like hoisting. But taking the time to learn how hoisting works will make JavaScript’s behavior much more intuitive, and it will protect your programs from a host of tricky bugs. Understanding hoisting will also help you discern which types of variables are best suited to different situations.

Hoisting is a mental model for how JavaScript executes the code in a program. It is worth emphasizing that it is just a mental model — a very useful one. JavaScript does not technically process code in the way that the hoisting paradigm describes, but analyzing code through the lens of hoisting makes JavaScript’s behavior much easier to understand. There are a few edge cases that hoisting does not account for, but it is sufficient in the vast majority of cases.

Before explaining exactly what hoisting is, let’s take a look at how it can affect a program:

On lines 1–3 we declare a constant variable printName and initialize it to an anonymous function expression. Then printName() is invoked and passed the string 'Rocky' as an argument. Inside the function, 'Rocky' is assigned to name and then passed as an argument to console.log() which displays "Rocky" on the console. There is nothing surprising here. But what happens if we re-organize the code?

Okay, so it looks like we cannot call the function in printName before the variable is declared in the program. Fair enough. What about if we change the function’s implementation from a function expression to a function declaration?

All of a sudden, we can call the printName() function before its definition gets executed in the program. This seemingly strange behavior is the result of hoisting.

The reason JavaScript knows about the printName() function before lines 3–5 run is that hoisting occurs before the program actually begins executing. JavaScript processes a program in two distinct phases. The second phase is the execution phase — when the program runs the code line-by-line. Hoisting happens before that during the creation phase. JavaScript searches through the program and identifies all of the places where a new variable is declared. It then hoists the variable declaration to the top of its scope. To put it another way, JavaScript treats the variable as if it was declared at the very top of the scope it is in. If that doesn’t make sense immediately, that is okay. Follow along with the next few examples, and it will become clear pretty quickly.

Function declarations

JavaScript’s hoisting behavior is slightly different for each kind of variable. Let’s begin with function declarations. Recall that function declaration syntax causes JavaScript to declare a variable with the same name as the function, and then initialize that variable to the function itself. When that variable is hoisted, it is hoisted to the top of its scope, and it brings its function value along with it. Take a look at the following example:

Now check out what the code looks like after printName is hoisted:

The second code snippet represents how JavaScript views the code when the execution phase begins, and the code starts running line-by-line. The declaration of printName is hoisted above its invocation, no error is raised, and printName() can be called successfully.

As discussed in my previous article, variables created from function declarations have function scope — they are only available to be called inside the function body wherein they are declared, and any more narrow scope. This has an effect how nested functions are hoisted:

Here is what JavaScript sees after hoisting printName :

Since the printName declaration is hoisted to the top of greetTheCat’s function body, it can be called anywhere inside that function. printName cannot however, be called outside of that function body, as this would violate the rules of function-scoping.

var declarations

Variables declared using the var keyword are not hoisted in the same way as function declarations. When a variable is declared with var, the declaration is hoisted to the top if its scope, but it is given the value undefined. The variable remains set to undefined until the line where the variable is declared in the program executes. Assuming the declaration includes an initializer, only then is the variable re-assigned to the initializer’s value. Observe:

After hoisting, the code looks like this:

Let’s compare the two code snippets. On line 4 in the first snippet, a variable cat is declared. We see in the second snippet that the variable declaration is hoisted to the top of the function. As with any variable declaration without an initializer, the variable is implicitly initialized to undefined. That is why cat evaluates to undefined when passed to console.log(). What happens next is interesting — when the line where cat is declared in the program runs (line 4 in the first snippet), it is treated instead like a re-assignment. cat gets re-assigned to 'Rocky'. Taken altogether, this behavior is unique to how var variables are hoisted.

One more example will drive this home. Take a look at the following code, and try to guess what the output is before looking at the explanation.

If you need a hint, remember that function-scoped variables ignore blocks, and only recognize function bodies when it comes to scope.

Here is what that code looks like after hoisting:

The code logs "The cat wants undefined." to the console. Because var declares variables with function scope, the variable declaration is hoisted to the top of the global scope, completely ignoring the block. However, since the if condition is falsy, the block where thing is declared never runs. This causes thing to remain set to undefined, and that value is interpolated into the string which is logged to the console. Pretty nifty!

let and const declarations

Are you still wondering why our example at the beginning threw an error when we called a constant early? We are just about to get there. Out of all the kinds of variables, let and const have the most unexpected behavior when it comes to hoisting. Check it out:

“Wait, wait — that isn’t unexpected” you might be saying. Out of all of the behaviors we have seen, doesn’t this one make the most sense? It appears that JavaScript doesn’t hoist let and const variables — it won’t let you reference them before they are declared in the program.

That’s a good point, but let’s not get ahead of ourselves. Look what happens when we reference a variable without declaring it anywhere at all:

That is a different error message than we got before! The first error message implied that printName existed somewhere, it just had not been initialized to a value yet. This is the weirdness of how let and const variables are hoisted.

When a variable is declared with let or const, it is hoisted to the top of its scope, but it is not given any value at all (not even undefined). It remains uninitialized until the line where it is declared in the program runs. At that point it is given a value of undefined , or the value of the initializer if there is one.

The stretch of code between the top of a let or const variable’s scope and the place where it is declared in the program is called the temporal dead zone or TDZ. If you try to call the variable in this zone, JavaScript will raise the special kind of ReferenceError that we saw above.

We have covered a lot of ground. Before we wrap up, let’s look at one more hoisting example which illustrates a funky edge case. It turns out that JavaScript will let you declare two variables with the same name as long as neither of them are declared with let or const. This can lead to some bizarre behavior, but it gives us a glimpse of another nuance of hoisting.

The code looks like this after hoisting:

When a function declaration and a var variable declaration occur with the same variable name in the same scope, the function declaration is hoisted first. It is used to declare the variable, and the variable is initialized to the function as usual. The var declaration is ignored! If the var declaration includes an initializer, the initializer remains, in the form of a re-assignment. If there is no initializer, the var declaration is treated as if it never existed.

Hoisting can be a confusing topic, but that is one of the reasons why it is important to form a solid mental model of how it works. Having a strong grasp of this topic — strong enough that it becomes intuitive — will save you from some frustrating bugs in your programs. It will also allow you to make a better-informed choice about what kind of variable to use in a given situation. As with many topics in programming, practicing until you have mastered the fundamentals is a worthwhile investment in your long-term success. It also makes writing software a more enjoyable experience.

This is the second of three articles on selected topics that are fundamental to JavaScript. Feel free to checkout the first article on variables and scope, or the third article on closures if those topics interest you!

--

--