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
var | let | const | |
|---|---|---|---|
| Scope | Function | Block { } | Block { } |
| Hoisted | Yes, set to undefined | Yes, but not usable yet | Yes, but not usable yet |
| Use before declaring | Gives undefined | ReferenceError | ReferenceError |
| Re-declare in same scope | Allowed | Error | Error |
| Reassign | Yes | Yes | No |
Creates a window property at top level | Yes | No | No |
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
console.log(a); // undefined
var a = 1;
console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 2;var ais hoisted and set toundefined, so reading it early silently givesundefined, which hides bugs.let bis 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.constbehaves likelet, and must also be given a value on the same line.
Function declarations vs expressions
| Kind | Example | Can call before it's defined? |
|---|---|---|
| Function declaration | function greet() {} | Yes, the whole function is hoisted |
| Function expression | const greet = function () {}; | No, follows const rules |
| Arrow function | const greet = () => {}; | No, follows const rules |
| Class | class 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:
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 2let 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 mode | With strict mode |
|---|---|
| Assigning to an undeclared variable creates a global | ReferenceError |
| Assigning to a read-only property fails silently | TypeError |
this in a plain function call is window | this is undefined |
| Duplicate parameter names allowed | SyntaxError |
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 useJavaScript Hoisting Summary
- Hoisting means declarations are processed before code runs.
varis function-scoped and hoisted asundefined;letandconstare block-scoped.- Using
letorconstbefore 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.
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";Line-by-line explanation
- Line 1
var earlyexists already, but isundefined. - Line 5Calling a function declaration before its line works.
- Lines 10 to 13Reading a
letvariable in its temporal dead zone throws a ReferenceError.
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()));Line-by-line explanation
- Line 2
varignores the block and belongs to the surrounding scope. - Line 6
keptdoesn't exist outside the block. - Lines 9 to 10All three functions share one
i, which ends at 3. - Lines 13 to 14Each iteration gets its own
j.
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());Line-by-line explanation
- Line 2Without strict mode, the typo silently creates a global.
- Line 8Strict mode can be switched on for a single function.
- 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
init(); // ReferenceError
const init = () => console.log("ready");const init = () => console.log("ready");
init();Thinking const makes values unchangeable
const cart = [];
cart = ["apple"]; // TypeError: assignment to constantconst cart = [];
cart.push("apple"); // fine: the array's contents can changeconst stops reassignment of the variable, not changes inside an object or array.
Using var in loops with callbacks
for (var i = 0; i < buttons.length; i++) {
buttons[i].onclick = () => console.log(i); // always the last value
}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.
Declare a value that will never be reassigned.
TAX_RATE = 0.2;
console.log(TAX_RATE);
const by default.What is logged?
console.log(typeof x, typeof y);
var x = 1;
function y() {}What scope does let have?
What is the temporal dead zone?
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]));- Each handler keeps its own value
- addUp still works
- total is not a global
JavaScript Hoisting Quiz
5 questions to check what you learned.
Which should you use by default for new variables?
- const
- var
- let
- global
Which can be called before the line it's defined on?
- const save = () => {}
- const save = function () {}
- function save() {}
- class Save {}
What does "use strict" do to an assignment to an undeclared variable?
- Creates a global
- Ignores it
- Converts it to let
- Throws a ReferenceError
Are JavaScript modules in strict mode?
- Yes, automatically
- Only with "use strict"
- No, never
- Only in Node.js
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:
- It uses
"use strict"insidebuildMenu - No
varremains anywhere buildMenu(["Tea", "Cake"])returns["1. Tea", "2. Cake"]- Each function in
buildMenu.clicksreturns 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"]));- Builds numbered lines
- Each click function keeps its own number
- No var and uses strict mode