Hoisting, var vs let vs const and Strict Mode

Understand hoisting, the temporal dead zone, why let and const replaced var, how function declarations and expressions differ, and what "use strict" changes.

Lesson 20 of 56Intermediate16 min read

Why can you call some functions before the line where they're written, but not others? Why does a var variable log undefined before it's assigned, while a let variable throws an error? And why did JavaScript need three different ways to declare a variable in the first place? The answers all come from one behaviour called hoisting, plus some history.

JavaScript reads your code in two passes. First it scans for declarations and sets up the variables and functions. Then it runs the code line by line. The effect is as if declarations were hoisted, lifted to the top of their scope. But var, let, const and the different kinds of functions are hoisted in different ways, and those differences explain a whole family of confusing bugs.

These topics also come up constantly in job interviews. In this lesson you'll learn exactly how each declaration behaves, why modern code uses const and let, what the temporal dead zone is, and how strict mode turns silent mistakes into clear errors. You'll build on what you learned in scope.

var, let and const compared

varletconst
ScopeFunctionBlock { }Block { }
HoistedYes, set to undefinedYes, but not usable yetYes, but not usable yet
Use before declaringGives undefinedReferenceErrorReferenceError
Re-declare in same scopeAllowedErrorError
ReassignYesYesNo
Creates a window property at top levelYesNoNo

let and const arrived in 2015 to fix var's surprises. Today the advice is simple: use const by default, let when the value must change, and avoid var in new code.

Hoisting and the temporal dead zone

JavaScript
console.log(a); // undefined
var a = 1;

console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 2;
  • var a is hoisted and set to undefined, so reading it early silently gives undefined, which hides bugs.
  • let b is hoisted too, but it stays uninitialised until its line runs. The gap between the start of the block and the declaration is called the temporal dead zone (TDZ). Reading it there throws an error, which is good: you find the bug immediately.
  • const behaves like let, and must also be given a value on the same line.

Function declarations vs expressions

KindExampleCan call before it's defined?
Function declarationfunction greet() {}Yes, the whole function is hoisted
Function expressionconst greet = function () {};No, follows const rules
Arrow functionconst greet = () => {};No, follows const rules
Classclass User {}No, classes have a TDZ too

Because declarations are fully hoisted, you can put the main code at the top of a file and helper functions underneath. Expressions and arrow functions must be defined before they're used.

The classic var loop bug

var is function-scoped, so there is only one i shared by every iteration:

JavaScript
for (var i = 0; i < 3; i++) setTimeout(() => console.log(i), 0); // 3 3 3
for (let j = 0; j < 3; j++) setTimeout(() => console.log(j), 0); // 0 1 2

let creates a fresh j for each loop iteration, so each callback remembers its own value. This one example shows why let replaced var.

Strict mode

Adding "use strict"; at the top of a file or function turns on strict mode, which makes JavaScript less forgiving:

Without strict modeWith strict mode
Assigning to an undeclared variable creates a globalReferenceError
Assigning to a read-only property fails silentlyTypeError
this in a plain function call is windowthis is undefined
Duplicate parameter names allowedSyntaxError

You rarely need to type it: JavaScript modules and classes are always in strict mode automatically.

JavaScript Hoisting Syntax

Declarations

"use strict";
const LIMIT = 10;      // can't be reassigned
let count = 0;         // can be reassigned
count++;

sayHi();               // works: declaration is hoisted
function sayHi() { console.log("Hi"); }

const add = (a, b) => a + b; // must be defined before use

JavaScript Hoisting Summary

  • Hoisting means declarations are processed before code runs.
  • var is function-scoped and hoisted as undefined; let and const are block-scoped.
  • Using let or const before their line throws an error: the temporal dead zone.
  • Function declarations are fully hoisted; function expressions and arrow functions are not.
  • Strict mode turns silent mistakes into errors, and modules use it automatically.

JavaScript Hoisting Examples

Each example runs in the page. Edit the code and press Run to see what changes.

Example 1

Hoisting in action

console.log("var before:", typeof early, early);
var early = "now set";
console.log("var after:", early);

greet("Ana");
function greet(name) {
  console.log("Hello,", name);
}

try {
  console.log(later);
} catch (err) {
  console.log("let before:", err.name, "-", err.message);
}
let later = "ready";
ConsoleEdit the code, then press Run or Ctrl+Enter
Line-by-line explanation
  1. Line 1var early exists already, but is undefined.
  2. Line 5Calling a function declaration before its line works.
  3. Lines 10 to 13Reading a let variable in its temporal dead zone throws a ReferenceError.
Example 2

Block scope and the loop bug

if (true) {
  var leaked = "var escapes the block";
  let kept = "let stays inside";
}
console.log(leaked);
console.log(typeof kept);

const withVar = [];
for (var i = 0; i < 3; i++) withVar.push(() => i);
console.log("var loop:", withVar.map(f => f()));

const withLet = [];
for (let j = 0; j < 3; j++) withLet.push(() => j);
console.log("let loop:", withLet.map(f => f()));
ConsoleEdit the code, then press Run or Ctrl+Enter
Line-by-line explanation
  1. Line 2var ignores the block and belongs to the surrounding scope.
  2. Line 6kept doesn't exist outside the block.
  3. Lines 9 to 10All three functions share one i, which ends at 3.
  4. Lines 13 to 14Each iteration gets its own j.
Example 3

Strict mode catches mistakes

function sloppy() {
  totl = 5; // typo creates a global variable
  return typeof totl;
}
console.log("sloppy:", sloppy());

function strict() {
  "use strict";
  try {
    totl2 = 5;
  } catch (err) {
    return err.name + ": " + err.message;
  }
}
console.log("strict:", strict());
ConsoleEdit the code, then press Run or Ctrl+Enter
Line-by-line explanation
  1. Line 2Without strict mode, the typo silently creates a global.
  2. Line 8Strict mode can be switched on for a single function.
  3. Line 10The same typo now throws a ReferenceError, so you find it straight away.

Common JavaScript Hoisting Mistakes

Calling an arrow function before it's defined

Don't
init(); // ReferenceError
const init = () => console.log("ready");
Do
const init = () => console.log("ready");
init();

Thinking const makes values unchangeable

Don't
const cart = [];
cart = ["apple"]; // TypeError: assignment to constant
Do
const cart = [];
cart.push("apple"); // fine: the array's contents can change

const stops reassignment of the variable, not changes inside an object or array.

Using var in loops with callbacks

Don't
for (var i = 0; i < buttons.length; i++) {
  buttons[i].onclick = () => console.log(i); // always the last value
}
Do
for (let i = 0; i < buttons.length; i++) {
  buttons[i].onclick = () => console.log(i);
}

JavaScript Hoisting in Real Projects

Why this matters on the job

Code linters such as ESLint include rules like no-var and prefer-const, and most teams turn them on. Older libraries and tutorials still use var, so you need to recognise its behaviour when reading them. Hoisting, the TDZ and the var loop bug are among the most common JavaScript interview questions.

JavaScript Hoisting Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Fill in the blankseasy

Declare a value that will never be reassigned.

 TAX_RATE = 0.2;
console.log(TAX_RATE);
Predict the outputmedium

What is logged?

console.log(typeof x, typeof y);
var x = 1;
function y() {}
Multiple choiceeasy

What scope does let have?

Multiple choicemedium

What is the temporal dead zone?

Fix the bugmedium

makeHandlers should return three functions that return 0, 1 and 2, and total should not leak into the global scope. Fix it.

function makeHandlers() {
  const list = [];
  for (var i = 0; i < 3; i++) list.push(() => i);
  return list;
}
function addUp(nums) {
  total = 0;
  for (const n of nums) total += n;
  return total;
}
console.log(makeHandlers().map(f => f()), addUp([1, 2]));
ConsoleEdit the code, then press Run or Ctrl+Enter
Checks
  • Each handler keeps its own value
  • addUp still works
  • total is not a global

JavaScript Hoisting Quiz

5 questions to check what you learned.

  1. Which should you use by default for new variables?

    • const
    • var
    • let
    • global
  2. Which can be called before the line it's defined on?

    • const save = () => {}
    • const save = function () {}
    • function save() {}
    • class Save {}
  3. What does "use strict" do to an assignment to an undeclared variable?

    • Creates a global
    • Ignores it
    • Converts it to let
    • Throws a ReferenceError
  4. Are JavaScript modules in strict mode?

    • Yes, automatically
    • Only with "use strict"
    • No, never
    • Only in Node.js
  5. Why does for (var i...) with setTimeout log the same number three times?

    • setTimeout is broken
    • There's one shared i, and the callbacks run after the loop has finished
    • var is slower
    • The loop runs three times too fast

JavaScript Hoisting Challenge

Modernise old code

Rewrite this old-style code so that:

  1. It uses "use strict" inside buildMenu
  2. No var remains anywhere
  3. buildMenu(["Tea", "Cake"]) returns ["1. Tea", "2. Cake"]
  4. Each function in buildMenu.clicks returns its own item number when called (the first returns 1, the second 2)
function buildMenu(items) {
  var lines = [];
  var clicks = [];
  for (var i = 0; i < items.length; i++) {
    lines.push(i + 1 + ". " + items[i]);
    clicks.push(function () { return i + 1; });
  }
  buildMenu.clicks = clicks;
  return lines;
}
console.log(buildMenu(["Tea", "Cake"]));
ConsoleEdit the code, then press Run or Ctrl+Enter
Checks
  • Builds numbered lines
  • Each click function keeps its own number
  • No var and uses strict mode