Hoisting in JavaScript: Predict Output with var, let, const and Functions
Build a reliable scope-setup model for JavaScript hoisting, then use it to trace declarations, diagnose three common errors and solve output questions.
KnowledgeGate Team
Exam prep & CS education

Why does console.log(total) above var total = 8 print undefined, while accessing score above let score = 42 throws an error? The slogan "declarations move to the top" is too crude to predict real output. A better model follows scope setup and statement execution separately. JavaScript's setup and execution rules determine how var, let, const, function declarations and arrow functions behave, including the common failures caused by reading or calling bindings before initialization.
What hoisting in JavaScript actually means
Hoisting names what happens when JavaScript processes declarations while setting up a scope, before executing its statements. The engine does not physically rearrange source lines.
Keep two events separate: creating a binding in a scope, and running the initializer that supplies its value.
Declaration form | Scope and setup-time state |
|---|---|
| Function scoped, or script-global at the top level; initialized to |
| Block scoped; created but uninitialized in the temporal dead zone |
| Block scoped; created but uninitialized, and must receive a value at its declaration |
Function declaration | Bound to the complete function before statements execute |
Function expression or arrow function | Follows the rule of the variable that stores it |
The examples use ordinary scripts, functions and blocks. Block-level function-declaration edge cases and module-loading rules require separate treatment. Other JavaScript constructs can follow different rules.
Trace one worked example from scope setup to final output
Run this example exactly as written:
console.log(total);
console.log(add(4, 6));
var total = 8;
function add(a, b) {
return a + b;
}
console.log(total);It prints:
undefined
10
8During scope setup, JavaScript creates total with undefined and binds add to its complete function. During execution, the first log reads undefined. The second calls add(4, 6), so 4 + 6 = 10. The initializer then assigns 8 to total, and the final log reads 8.
The var total declaration is available early, but its initializer = 8 is not. The function declaration is callable early because its binding already contains the function body, not undefined.

Why let and const are hoisted but cannot be read early
The temporal dead zone is the interval from entering a block until a let or const declaration is initialized. The binding exists during that interval, but accessing it throws ReferenceError instead of returning undefined.
The earlier JavaScript Variables: let, const and var Scope with Runnable Examples owns declaration choice, reassignment, const objects and loop bindings. This tutorial stays with early-access state and callable bindings.
{
try {
console.log(score);
} catch (error) {
console.log(error.name);
}
let score = 42;
const limit = 3;
console.log(score + limit);
}The output is:
ReferenceError
45At the first access, score exists but is uninitialized. The caught error lets execution continue. Its declaration gives it 42, while limit receives 3, so 42 + 3 = 45. A const binding cannot be reassigned after initialization, but it is still created during scope setup.
Function declarations, function expressions and arrow functions
A function declaration is bound to its function, while a function expression or arrow function follows the variable's binding rule:
The earlier Functions and Arrow Functions in JavaScript tutorial compares syntax, parameters, closures and lexical this. This section isolates when each binding becomes callable.
console.log(square(5));
try {
console.log(double(5));
} catch (error) {
console.log(error.name);
}
function square(n) {
return n * n;
}
var double = (n) => n * 2;
console.log(double(5));It prints:
25
TypeError
10square already contains the function, so 5 * 5 = 25. The early double binding contains undefined; calling it produces TypeError. After its initializer runs, double(5) calculates 5 * 2 = 10.
Replacing var double with let double changes the early failure to ReferenceError, because the binding is in the temporal dead zone. Arrow functions are not simply "not hoisted". The variable is processed during setup, while its function value arrives during initialization.
Hoisting stops at scope boundaries
Predict scope before trying to predict output. var belongs to the nearest function scope, while let and const belong to the nearest block.
let label = "outer";
function reveal(flag) {
if (flag) {
var status = "ready";
let label = "inner";
console.log(label);
}
console.log(status);
console.log(label);
}
reveal(true);The output is inner, ready, outer. Function-scoped status starts as undefined and becomes "ready" because flag is true. The inner label exists only in the if block. After it ends, lookup reaches outer label, whose value is "outer".
Hoisting never makes a binding global. Start every trace by drawing the current script, function and block scopes, then place each declaration in its actual scope.

Diagnose the three common hoisting failures
Name the binding state before naming the result:
Code shape | State at access | Result |
|---|---|---|
| Existing binding containing | Prints |
| Existing but uninitialized binding |
|
| Existing binding containing |
|
Shadowing can make this less obvious:
let count = 10;
function show() {
try {
console.log(count);
} catch (error) {
console.log(error.name);
}
let count = 20;
console.log(count);
}
show();The output is ReferenceError, then 20. Inner count shadows outer 10 throughout the function body, including its own temporal dead zone.
One typeof distinction matters: typeof neverDeclared returns "undefined", but typeof later before let later = 3 throws ReferenceError.
Use this debugging routine: locate the scope, classify each declaration, mark its setup-time state, execute initializers in order, then calculate calls and logs.
How output questions test hoisting, with three exercises
Treat these as practice formats, not as an official exam or company pattern. The same trace-before-run habit also helps with Python Output-Based Questions.
Exercise A: local var shadowing
var x = 1;
function demo() {
console.log(x);
var x = 2;
console.log(x);
}
demo();Answer: undefined, then 2. Local var x shadows global x = 1, starts as undefined, then becomes 2.
Exercise B: a const arrow function
try {
greet("KG");
} catch (error) {
console.log(error.name);
}
const greet = (name) => console.log(name);
greet("KG");Answer: ReferenceError, then KG. The first access hits the temporal dead zone. The catch allows initialization to run, so the second call succeeds.
Exercise C: function declaration plus var
function pick() {
return 7;
}
var pick;
console.log(pick());Answer: 7. The function declaration supplies the setup-time value. Later var pick; has no initializer, so it does not overwrite the function. Write the binding state beside each line instead of relying on a slogan.
Hoisting in JavaScript: the short version and next step
Keep these five rules:
Declarations are processed per scope.
A
varbinding starts asundefined.letandconststay uninitialized until their declarations run.Function declarations are available before their source line in the ordinary examples shown.
Function expressions and arrow functions become callable only after the storing variable is initialized.
Run all five teaching snippets unchanged. Then change exactly one declaration at a time: first var total to let total, then var double to let double. Predict the error before rerunning so the exercise stays about binding state.
For a direct learning path, use the Complete JavaScript Course. If you are ready to apply JavaScript across a wider stack, continue with the MERN Stack Course - Full Stack Development.
Keep learning

CSS Container Queries: Build a React Card That Adapts to Its Container
Build one portable React card that responds to the space its parent provides. Follow its layout, padding and title size at three exact container widths.

Regular Expressions in JavaScript: Match, Extract, Replace, and Validate
Learn JavaScript regex by tracing what each token consumes. Build a validator and a named-group log parser, then practise extraction, replacement, flags, and common traps.

Functions and Arrow Functions in JavaScript: Syntax, Scope and Runnable Examples
Build function declarations, expressions and arrows in JavaScript, then trace parameters, closures, lexical this and array callbacks through exact outputs.

Modules in JavaScript: Import, Export, and Runnable Examples
Build JavaScript modules from scratch with a shopping-cart project that runs in Node.js and the browser. Learn live bindings, re-exports, lazy loading, and practical fixes.