Complete JavaScript Course Module 5: Beginner: Functions

JavaScript Hoisting and Strict Mode

  • Beginner
  • Lesson 5.5
  • 6 min read
  • 21 of 76 in the course

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:

DeclarationHoisted?Value before its lineUsing it early gives
function greet() {}Yes, fullyThe whole functionIt works
var nameYesundefinedundefined (no error)
let nameYes, but not readyNone (in the TDZ)ReferenceError
const nameYes, but not readyNone (in the TDZ)ReferenceError
class Card {}Yes, but not readyNone (in the TDZ)ReferenceError
const fn = () => {}Like any constNone (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 undefined that breaks something three functions later. This is one more reason to use let and const instead of var.

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 globalReferenceError
this in a plain function call is the global windowthis is undefined
Writing to a read-only or frozen property fails silentlyTypeError
Duplicate parameter names function f(a, a) are allowedSyntaxError
Old octal numbers like 010 are allowedSyntaxError
The with statement is allowedSyntaxError
Future keywords like let, static and implements can be variable namesSyntaxError

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 with import.
  • 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; var is hoisted as undefined; let, const and class are 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.