Hoisting is the JavaScript behavior that makes a declared name exist from the first line of its scope, even when the declaration is written further down. Before a function or a module runs, the engine registers every variable, function and class declared in it. What happens when code uses a declared name early depends on the keyword.
- A var variable reads as undefined.
- A let, const or class name throws a ReferenceError.
- A function declaration is already fully usable.
JavaScript variable hoisting explains several puzzling bugs. For example, a variable is undefined although it gets a value a few lines later, or an error message says “Cannot access ‘x’ before initialization”.
The following example shows every hoisting case in one JavaScript snippet, with the results from Node.js 22. The commented lines throw the error named in the comment.
// 1. var: declaration hoisted, value is undefined
const a = early; // a = undefined
var early = 10;
// 2. let and const: temporal dead zone
// const b = later; // ReferenceError: Cannot access 'later' before initialization
let later = 20;
// 3. Function declaration: fully hoisted
const sum = add(2, 3); // sum = 5
function add(x, y) { return x + y; }
// 4. Function expression: only its var is hoisted
// const product = multiply(2, 3); // TypeError: multiply is not a function
var multiply = function (x, y) { return x * y; };
// 5. Class: temporal dead zone
// const user = new User(); // ReferenceError: Cannot access 'User' before initialization
class User {}
// 6. typeof of an undeclared name vs a name in the temporal dead zone
const kind = typeof notDeclared; // "undefined"
// const sizeKind = typeof size; // ReferenceError: Cannot access 'size' before initialization
let size = 3;
Notice that the var in step 1 gives undefined without any error, whereas let, const and class fail with a clear message.
Next, we go through each case and look at the temporal dead zone. At the end, we see which of the hoisting mistakes the TypeScript 7 compiler reports before the code runs.
1. What Hoisting Really Does
Many tutorials describe hoisting as “moving declarations to the top”. The engine does not move any code. It runs a scope in two steps. First, it reads the whole function (or module) and creates an entry for every declared name, and then it runs the statements from top to bottom. Because the names already exist in the second step, code can refer to them before their declaration line.
The keyword decides what value a hoisted name has between those two steps, and the table groups the declarations by that value.
| Declaration | Usable before its line? | Value before its line |
|---|---|---|
| var | Yes | undefined |
| let, const | No | Reading it throws ReferenceError (temporal dead zone) |
| class | No | Reading it throws ReferenceError (temporal dead zone) |
| function declaration (also async and generator functions) | Yes | The complete function |
| import | Yes | The imported binding; the imported module runs first |
Hoisting works per scope. A var is hoisted to the top of its function, whereas let, const and class are hoisted to the top of their block. Nothing is hoisted out of a function into the code that calls it.
2. Hoisting of var Variables
A line such as var early = 10; contains two separate parts, a declaration (var early) and an assignment (early = 10). Only the declaration is hoisted, whereas the assignment stays where it is written and runs when execution reaches that line. Until then, the variable holds undefined.
const a = early; // a = undefined
var early = 10;
The engine runs the snippet as if we had written the declaration at the top of the scope.
var early; // created when the scope starts
const a = early; // a = undefined
early = 10;
We might expect hoisting to skip a variable that is initialized in its declaration, but it does not. The declaration is always hoisted, and the initialization never is. For that reason, the result is undefined and not 10.
2.1. A Local var Hides the Outer Variable From the First Line
The var rule causes the most confusing hoisting bug. When a function declares a var with the same name as an outer variable, the local variable exists from the first line of the function. Every use of that name in the function refers to the local variable, even the uses before its declaration.
var fruit = "apple";
function pick() {
const before = fruit; // before = undefined
var fruit = "banana";
return before;
}
We expect before to be “apple”, the outer value, but it is undefined, because the local fruit already exists but has no value yet. The same bug shows up in old code when a function reads a global config and later declares its own var config for a local override. Renaming the local variable or declaring it with let at the top of the function removes the surprise.
3. let, const and the Temporal Dead Zone
Variables declared with let and const are hoisted too, but the engine does not initialize them. From the start of the block until the declaration line runs, the variable is in the temporal dead zone (TDZ). Any read or write in that zone throws a ReferenceError.
ReferenceError: Cannot access 'later' before initialization
The message itself shows that the variable is hoisted. If later were not known to the engine at that point, the message would be “later is not defined”, the error for a name that does not exist at all. The same zone applies to class declarations, so new User() before the class declaration fails with “Cannot access ‘User’ before initialization”.
The zone is about time, not about the position in the file. A function may refer to a const declared later in the file, as long as the function is called after the declaration has run.
const greet = () => greeting + ", Lokesh";
const greeting = "Hello";
const text = greet(); // text = "Hello, Lokesh"
Calling greet() before the const greeting line throws the ReferenceError. For that reason, helper functions that call each other work in any order in a file, whereas top-level code that runs when the module loads must come after the variables it reads.
The temporal dead zone also changes how typeof behaves. For a name that was never declared, typeof returns the string “undefined” without an error. For a let or const name in its temporal dead zone, typeof throws the same ReferenceError.
4. Hoisting of Functions
A function declaration is hoisted together with its body, so we can call it before its definition. We can use function hoisting to put the main logic at the top of a file and the helper functions at the bottom. For example, a checkout module starts with the total calculation and keeps small helpers such as addTax() at the end.
const total = addTax(100); // total = 118
function addTax(amount: number): number {
return amount * 1.18;
}
A function expression or an arrow function is not hoisted with its value, because it is a value assigned to a variable. Only the variable is hoisted, according to its keyword. With var multiply = function () {…}, the variable is undefined before the assignment, and calling it fails with a TypeError (multiply is not a function). With const multiply = …, the call fails earlier with the temporal dead zone ReferenceError. Arrow functions in TypeScript functions follow the same rules as any other var, let and const variable.
5. Hoisting Errors That TypeScript Reports
The TypeScript compiler knows the hoisting rules and reports use-before-declaration mistakes at compile time. With strict mode (the default in TypeScript 7), it reports even the var cases that JavaScript runs without an error. The snippets from sections 2 to 4, written as one TypeScript file, do not compile.
export function demo(): void {
const a = early;
var early = 10;
const b = later;
let later = 20;
const product = multiply(2, 3);
var multiply = function (x: number, y: number) { return x * y; };
const user = new User();
class User {}
console.log(a, b, product, user);
}
var fruit = "apple";
function pick(): string | undefined {
const before = fruit;
var fruit = "banana";
return before;
}
The compiler reports every early use.
src/e.ts(2,13): error TS2454: Variable 'early' is used before being assigned.
src/e.ts(5,13): error TS2448: Block-scoped variable 'later' used before its declaration.
src/e.ts(5,13): error TS2454: Variable 'later' is used before being assigned.
src/e.ts(8,19): error TS2454: Variable 'multiply' is used before being assigned.
src/e.ts(11,20): error TS2449: Class 'User' used before its declaration.
src/e.ts(19,18): error TS2454: Variable 'fruit' is used before being assigned.
TS2448 and TS2449 are the compile-time versions of the temporal dead zone error. TS2454 comes from strictNullChecks, because the compiler sees that the variable has no value yet at that line. The last error is the shadowing bug from section 2.1, which TypeScript catches because the local fruit is read before it is assigned.
TypeScript does not report the two safe cases.
- A call of a function declaration before its definition is safe.
- A closure that reads a const declared later is safe as long as it runs after the declaration.
The compiler cannot always know when a closure runs, so a call that happens too early still fails at runtime with the ReferenceError.
6. Practical Rules That Avoid Hoisting Bugs
Hoisting matters mostly when we read old code, and a few habits make it irrelevant in new code.
- Declare variables with const or let, never var. With let and const, a mistake fails with a clear ReferenceError or a compile error instead of an undefined value.
- Declare variables at the top of the block that uses them, before the first statement that reads them.
- Turn on the ESLint rule no-use-before-define to catch early uses that the compiler allows, such as calls of closures.
- Call a function stored in a const (an arrow function or a function expression) only after its declaration line has run. Function declarations do not have the order limit.
7. Example Code With Real Output
Because most hoisting cases are compile errors in TypeScript, the hoisting examples are mainly JavaScript. The file quick-reference.js holds the first snippet of the article, and hoisting.js runs each case separately (including the commented-out lines) and prints the result or the error. The TypeScript snippets from sections 3 and 4 are in the src folder and compile with TypeScript 7.0.2, and Node.js 22 runs everything.
npm install
npm start # compile and run the TypeScript snippets
npm run js # run js/quick-reference.js and js/hoisting.js
1. var before declaration: undefined
2. let before declaration: ReferenceError: Cannot access 'later' before initialization
3. function declaration: 5
4. function expression: TypeError: multiply is not a function
5. class before declaration: ReferenceError: Cannot access 'User' before initialization
6. local var shadows outer: undefined
7. typeof undeclared name: undefined
7. typeof in the TDZ: ReferenceError: Cannot access 'size' before initialization
8. Conclusion
Hoisting means that every declaration exists from the start of its scope. A var reads as undefined until its assignment runs, whereas let, const and class throw a ReferenceError inside the temporal dead zone. Function declarations work anywhere in their scope.
The undefined from var hides bugs, which is the main reason to prefer let and const. In TypeScript, strict mode turns most early uses into compile errors (TS2448, TS2449 and TS2454), so hoisting problems rarely reach runtime.
9. References
MDN documents the hoisting behavior of each kind of declaration, and the TypeScript handbook explains how the compiler treats var, let and const.
- MDN Glossary: Hoisting
- MDN: let, temporal dead zone
- MDN: var hoisting
- MDN: function declaration hoisting
- TypeScript Handbook: Variable Declarations
- ESLint: no-use-before-define
Happy Learning !!