In short
JavaScript hoisting explains why some variables and functions can be used before the line that creates them. Learn how var, let, const and functions hoist, the TDZ, and "use strict". What is hoisting in JavaScript?
JavaScript hoisting explains why some variables and functions can be used before the line that creates them. Learn how var, let, const and functions hoist, the TDZ, and "use strict".
What is hoisting in JavaScript?
Before JavaScript runs your code, it first reads through it and sets up every variable and function it finds in each scope. Hoisting is the name for this effect: it looks as if the declarations were lifted ("hoisted") to the top of their scope. Your code does not actually move. JavaScript simply knows about the names before it reaches the lines.
What matters is what each name holds before its line runs. That depends on how it was declared:
| Declaration | Hoisted? | Value before its line | Using it early gives |
|---|---|---|---|
function greet() {} | Yes, fully | The whole function | It works |
var name | Yes | undefined | undefined (no error) |
let name | Yes, but not ready | None (in the TDZ) | ReferenceError |
const name | Yes, but not ready | None (in the TDZ) | ReferenceError |
class Card {} | Yes, but not ready | None (in the TDZ) | ReferenceError |
const fn = () => {} | Like any const | None (in the TDZ) | ReferenceError |
You saw function hoisting briefly in the Functions lesson and the temporal dead zone in the Scope lesson. Let's look at each case properly.
Function hoisting: calling a function before it is written
Function declarations are hoisted completely, body and all. That is why you can call them above the line where you wrote them:
console.log(formatPrice(24.5)); // Output: $24.50
function formatPrice(amount) {
return `$${amount.toFixed(2)}`;
}
This is useful: you can put the main code at the top of a file and the helper functions below it, like a story first and the details after.
Function expressions and arrow functions stored in a variable follow the rules of that variable, not of functions:
// console.log(add(2, 3)); // ReferenceError: Cannot access 'add' before initialization
const add = (a, b) => a + b;
console.log(add(2, 3)); // Output: 5
// With var, the error is different and more confusing:
// sayHi(); // TypeError: sayHi is not a function
var sayHi = function () {
console.log("Hi!");
};
sayHi(); // Output: Hi!
With var, the name sayHi exists early but holds undefined, so calling it is like calling undefined(). That is why the error says "is not a function" instead of "not defined".
var vs let and const: hoisting and the temporal dead zone
var is hoisted and quietly set to undefined until its line runs. That hides bugs, because your code keeps going with a wrong value:
console.log(discount); // Output: undefined (no error, but no real value either)
var discount = 10;
console.log(discount); // Output: 10
JavaScript treats this as if you had written var discount; at the top and discount = 10; later.
let and const are hoisted too, but they stay in the temporal dead zone (TDZ) from the start of their block until their line runs. "Temporal" means it is about time: the variable exists, but it is locked until JavaScript reaches the declaration. Touching it in that time throws an error:
const city = "Paris";
function showCity() {
// console.log(city); // ReferenceError: Cannot access 'city' before initialization
const city = "Rome";
console.log(city);
}
showCity(); // Output: Rome
This example proves that let and const are hoisted. If they were not, the commented line would simply read the outer city and print "Paris". Instead, JavaScript already knows a local city exists in this function, so the outer one is hidden and the local one is not ready yet.
Even typeof, which is normally safe on unknown names, throws inside the TDZ:
console.log(typeof notCreatedAnywhere); // Output: undefined
// console.log(typeof total); // ReferenceError: Cannot access 'total' before initialization
let total = 0;
Tip: The TDZ is your friend. A loud error at the right line is much easier to fix than a silent
undefinedthat breaks something three functions later. This is one more reason to useletandconstinstead ofvar.
What does "use strict" do?
Strict mode is a safer version of JavaScript that turns silent mistakes into real errors and removes some confusing old features. You switch it on by writing the string "use strict"; as the very first statement of a script or a function.
"use strict";
let userName = "Ava";
usrName = "Ben"; // typo!
// ReferenceError: usrName is not defined
Without strict mode, that typo would silently create a brand-new global variable called usrName, and userName would stay "Ava". You might spend an hour looking for the bug. Strict mode stops at the exact line instead.
The main things strict mode changes:
| Without strict mode (sloppy mode) | With strict mode |
|---|---|
| Assigning to an undeclared variable creates a global | ReferenceError |
this in a plain function call is the global window | this is undefined |
| Writing to a read-only or frozen property fails silently | TypeError |
Duplicate parameter names function f(a, a) are allowed | SyntaxError |
Old octal numbers like 010 are allowed | SyntaxError |
The with statement is allowed | SyntaxError |
Future keywords like let, static and implements can be variable names | SyntaxError |
Here are two of these changes in action. Paste the whole block into the console in one go:
"use strict";
function whoAmI() {
return this;
}
console.log(whoAmI()); // Output: undefined (would be the window object in sloppy mode)
const settings = Object.freeze({ theme: "dark" });
try {
settings.theme = "light";
} catch (error) {
console.log(error.name); // Output: TypeError
}
console.log(settings.theme); // Output: dark
In sloppy mode, the second change would be ignored with no message at all. Strict mode tells you that the object is frozen.
You can also turn on strict mode for just one function by putting the string at the top of its body:
function calculateTotal(prices) {
"use strict";
let sum = 0;
for (const price of prices) {
sum += price;
}
return sum;
}
console.log(calculateTotal([10, 15, 5])); // Output: 30
Common mistake: Putting
"use strict";below other code. It only works as the first statement of a file or function; anywhere else it is just a string that does nothing.
Modules and classes are strict automatically
You often do not need to write "use strict" at all. Two kinds of modern code are always in strict mode:
- JavaScript modules: any file loaded with
<script type="module">or brought in withimport. - Class bodies: all code inside
class { ... }, including its methods.
<!DOCTYPE html>
<html lang="en">
<body>
<script type="module">
// No "use strict" needed: modules are strict already
try {
price = 20;
} catch (error) {
console.log(error.message);
}
</script>
</body>
</html>
price is not defined
Since most modern projects use modules (and tools like Vite create them for you), your code is probably strict already. For an old-style <script> tag without type="module", add "use strict"; at the top yourself. You can read the full list of changes on MDN's strict mode page.
Practice exercise
Predict the output of this code, line by line, before you run it. Then run it in the console and compare:
console.log(typeof greet);
console.log(score);
var score = 5;
function greet() {
return "Hello";
}
console.log(greet());
console.log(level);
let level = 1;
Then add "use strict"; to the top, add a line totl = 10; (a typo) and see what changes.
Hint: greet is a fully hoisted function, score is a var, and level is a let in the TDZ, so the last console.log stops the script.
Key takeaways
- Hoisting means JavaScript knows every declared name before running the code in that scope.
- Function declarations are fully hoisted;
varis hoisted asundefined;let,constandclassare hoisted but locked in the TDZ. - Function expressions and arrow functions follow the rules of the variable that holds them.
"use strict"turns silent mistakes into errors and must be the first statement.- Modules and class bodies are always strict, so modern code is usually strict by default.