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

Updated 26 Oct 20266 min read

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

var

Function scoped, or script-global at the top level; initialized to undefined

let

Block scoped; created but uninitialized in the temporal dead zone

const

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:

javascript
console.log(total);
console.log(add(4, 6));

var total = 8;

function add(a, b) {
  return a + b;
}

console.log(total);

It prints:

Code
undefined
10
8

During 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.

Two panels: binding setup shows total as undefined and add holding its full function, then four execution steps ending with total 8.

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.

javascript
{
  try {
    console.log(score);
  } catch (error) {
    console.log(error.name);
  }

  let score = 42;
  const limit = 3;
  console.log(score + limit);
}

The output is:

Code
ReferenceError
45

At 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.

javascript
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:

Code
25
TypeError
10

square 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.

javascript
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.

Nested scope boxes for reveal(true), with arrows marking the output order: 1 inner from the if block, 2 ready, then 3 outer.

Diagnose the three common hoisting failures

Name the binding state before naming the result:

Code shape

State at access

Result

console.log(v); var v = 7;

Existing binding containing undefined

Prints undefined

console.log(l); let l = 7;

Existing but uninitialized binding

ReferenceError

fn(); var fn = () => 7;

Existing binding containing undefined is called

TypeError

Shadowing can make this less obvious:

javascript
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

javascript
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

javascript
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

javascript
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:

  1. Declarations are processed per scope.

  2. A var binding starts as undefined.

  3. let and const stay uninitialized until their declarations run.

  4. Function declarations are available before their source line in the ordinary examples shown.

  5. 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.