Hoisting and scoping: A deep dive into your interviewer's favourite question.

Table of contents
- Declaring a variable in javascript
- Variable Scope
- Variable Shadowing
- Javascript Execution Context
- Hoisting
- Temporal Dead Zone (TDZ)
- The TDZ is Actually a Good Thing
- Hoisting interview questions
- 1. Why doesn’t this function run as expected?
- 2. What’s happening here with let and shadowing?
- 3. What’s going on with these var declarations?
- 4. Why is this loop printing unexpected values?
- 5. Why does this throw when you access foo inside bar?
- 6. Why does this function behave differently based on where it’s declared?
- That’s a Wrap

If you have ever given a javascript interview chances are you have been asked about hoisting and variable scoping. You may know the definition but in order to solve those tricky "guess the output" questions you need to have a deep knowledge about how javascript works under the hood. Which is precisely what I am going to cover in this article. So put on your reading glasses, put away your phone and get ready to dive deep, unless of course you are reading this on your phone... don't put away your phone it that case ✌️. But if you are here just for the interview questions then skip to the end and go to town! Anyways let's begin.
Declaring a variable in javascript
Just in case you don't already know there are three ways of declaring a variable in javascript know to mankind (bots too these days 🤖): var, let and const.
var
Ah, var
, a relic of JavaScript's earlier days, presumably crafted by developers hammering away on Windows XP using Notepad as their trusty sidekick. It's notorious for turning straightforward code into a head-scratching puzzle.
Variables declared with var
can be both re-assigned and re-declared, setting the stage for some truly perplexing bugs. 😬
let
The cooler, younger sibling introduced in ES6. Variables declared with let
can be reassigned but not re-declared within the same scope. It's not necessary to initialise them at the point of declaration.
const
Also debuting in ES6, const
is for constants. Reassigning them will throw an error. You must initialise const
variables when you declare them.
Variable Scope
Scope defines where variables can be accessed within your code. If you declare a variable inside a function, it's only accessible within that function, not outside. Scope comes in three flavours: global, function, and block.
function daScope() {
const fullName = "Eren Yeager";
console.log(fullName); // Outputs: Eren Yeager
if (true) {
const titan = "mix veg";
console.log(titan); // Outputs: mix veg
}
console.log(titan); // Error: titan is not defined
}
daScope();
This code neatly illustrates block scoping. However, var
plays by its own rules, it's function-scoped, so it can be accessed anywhere within the function.
function daScope() {
const fullName = "Eren Yeager";
console.log(fullName); // Outputs: Eren Yeager
if (true) {
var titan = "mix veg";
console.log(titan); // Outputs: mix veg
}
console.log(titan); // Outputs: mix veg, no error!
}
daScope();
Think that’s quirky? Check out this scenario:
function greetingCreator() {
var greeting = "Hello";
if (true) {
var greeting = "Hi"; // This redeclares and reassigns `greeting` 🤯
console.log(greeting); // Output: "Hi"
}
function displayGreeting() {
console.log(greeting);
}
displayGreeting(); // Output: "Hi"
}
greetingCreator();
Both outputs are "Hi", demonstrating that var
does not recognise block scope, but only function scope. The greeting
inside the if
block affects the same greeting
declared at the function's start.
Variable Shadowing
To avoid the chaos of var
, we use let
, which enables variable shadowing. Here, if a variable declared in a nested scope has the same name as one in an outer scope, it shadows the outer variable without affecting it.
function greetingCreator() {
let greeting = "Hello";
if (true) {
let greeting = "Hi"; // A new, shadowed `greeting`
console.log(greeting); // Output: "Hi"
}
function displayGreeting() {
console.log(greeting);
}
displayGreeting(); // Output: "Hello"
}
greetingCreator();
Illegal Shadowing
And then there’s illegal shadowing, brought to you by var
, of course:
function greetingCreator() {
var greeting1 = "Konichiwa";
let greeting2 = "Hello";
if (true) {
let greeting1 = "Hajime Mashite"; // This is fine
console.log(greeting1); // Output: "Hajime Mashite"
var greeting2 = "Hi"; // Error: Identifier 'greeting2' has already been declared
console.log(greeting2);
}
function displayGreeting() {
console.log(greeting2);
}
displayGreeting();
}
greetingCreator();
Javascript Execution Context
JavaScript's execution context is one of the most fundamental concepts to grasp to understand how code executes, particularly how scopes, hoisting, closures, and asynchronous callbacks work. Let's take a detailed look at how JavaScript handles execution context through a step-by-step example.
Consider this JavaScript code snippet:
var x = 10;
let y = 20;
function multiply() {
var z = x * y;
console.log(z);
return z;
}
function display() {
let a = 5;
console.log(a);
multiply();
}
display();
console.log(x);
Here’s how JavaScript processes this code:
When the script loads, JavaScript creates a Global Execution Context. This global context performs two main actions during the creation phase:
Variable Environment Creation: Here, all the variable and function declarations are hoisted. Variables declared with
var
are initialised toundefined
, and functions are hoisted with their definitions. Variables declared withlet
andconst
remain uninitialised at this point and are in a temporal dead zone.Scope Chain Establishment: Sets up the scope chain, which determines the variable access throughout the code.
This Value Determination: For global execution context,
this
refers to the global object (window
in browsers,global
in Node.js).
After hoisting, the environment looks something like:
// Global Execution Context (GEC) starts
// Hoisted during the creation phase of GEC
var x = undefined; // 'var' variables are initialized as undefined
let y; // 'let' and 'const' are in Temporal Dead Zone (TDZ) and not initialized
function multiply() { /* function body is fully hoisted */ }
function display() { /* function body is fully hoisted */ }
// Execution phase of GEC begins
x = 10; // 'x' is now assigned a value
y = 20; // 'y' is assigned a value and comes out of TDZ
function display() {
// New Function Execution Context for display
let a = 5; // Local 'let' declaration, only exists within display
console.log(a); // Logs 5
multiply(); // Calls multiply, creating a new context for multiply
}
function multiply() {
// New Function Execution Context for multiply
var z = x * y; // 'var' declared and calculated using global x and y
console.log(z); // Logs 200
return z; // Returns 200, multiply context will be popped off the stack after return
}
display(); // Calls display, logs 5, then 200 from multiply
console.log(x); // Back in GEC, logs 10
// Both multiply and display Function Execution Contexts have been popped off
// Back in the GEC until script ends, at which point GEC is also remove
Global Execution Context Setup: At the start, JavaScript engine hoists function declarations and
var
variables in the global scope.Function Calls & Scope: When
display
andmultiply
functions are called, they each create their own execution contexts.display
Function Context: Contains its own local variables (e.g.,a
) and accesses functions and variables from the global scope.multiply
Function Context: Accesses global variables (x
andy
) and has its own local variable (z
).
Execution Flow: After
display
callsmultiply
,multiply
computes a result and finishes, popping its context off the stack. Thendisplay
finishes, returning control to the global context.End of Execution: After all function contexts resolve, and the script completes, the global context is finally popped off the execution stack.
Hoisting
Hoisting is JavaScript's behaviour of moving variable and function declarations to the top of their scope during the compilation phase, before any code is executed. This means that declarations are processed before any line of code runs, giving the illusion that they are "hoisted" to the top.
Imagine you walk into a classroom and see the teacher’s notes already written on the board. You didn’t see her write them, but somehow they were there before the class even started. That’s hoisting in JavaScript — the interpreter sneakily moves declarations to the top of their scope before any code is run.
But there’s a twist.
Hoisting doesn’t actually move your code physically. What it does is register certain declarations, just the declarations, not the initialisations, during the compilation phase, before your code runs. So when execution begins, JavaScript already knows about the existence of variables and functions, it just might not know their values yet.
Let’s break it down.
var
Hoisting
console.log(hoistedVar); // undefined
var hoistedVar = 42;
You might expect this to throw an error, but nope, it prints undefined
. Why? Because var hoistedVar
is hoisted to the top like this:
var hoistedVar; // Declaration is hoisted
console.log(hoistedVar); // undefined (default value for hoisted `var`)
hoistedVar = 42; // Initialization stays where it is
That’s classic var
behaviour. Its declarations are hoisted and automatically initialised with undefined
.
Function Hoisting
greet(); // "Hello!"
function greet() {
console.log("Hello!");
}
This works beautifully because function declarations are fully hoisted, both the name and the body. Think of it like JavaScript giving your functions a VIP backstage pass. They’re not just invited early, they come fully dressed and ready to perform.
But here's a curveball:
greet(); // TypeError: greet is not a function
var greet = function () {
console.log("Hi!");
};
Even though greet
is declared with var
, it’s hoisted as a variable, not as a function. So what gets hoisted?
var greet; // Only the declaration is hoisted
greet(); // greet is undefined at this point → TypeError
greet = function () {
console.log("Hi!");
};
This is why function declarations and function expressions behave differently when it comes to hoisting.
Now enter let
and const
...
You might think let
and const
are also hoisted. Technically, they are. But not in the way you'd hope.
console.log(a); // ❌ ReferenceError
let a = 10;
Even though the let a
declaration is hoisted, it's not accessible until the line where it's declared. The period between the start of the scope and the actual declaration is known as the Temporal Dead Zone (TDZ).
Variables in the TDZ cannot be accessed, not even to check if they exist.
if (true) {
console.log(name); // ❌ ReferenceError
let name = "Zoro";
}
Try to sneak a peek at name
before its declaration, and JavaScript slaps you with:
ReferenceError. But... why?
Well, welcome to the Temporal Dead Zone.
Temporal Dead Zone (TDZ)
Sounds scary, right? Like a place in a sci-fi movie where time and logic cease to exist. And honestly, that’s not far off.
The Temporal Dead Zone is the time between a variable being hoisted and being initialised, where accessing it will throw an error.
Here’s the deal:
let
andconst
are hoisted, but unlikevar
, they are not initialised withundefined
.Until the line where they are declared is actually executed, they're in the TDZ, a forbidden zone.
If you try to access them in that zone, JavaScript throws a ReferenceError, basically saying:
“Hold your horses! You can’t use this yet.”
Let’s break it down:
function sayHi() {
console.log(name); // 🚨 ReferenceError
let name = "Zoro";
}
sayHi();
Why does this error happen?
Even though name
is hoisted to the top of the function’s scope, it’s in the TDZ from the beginning of the scope until the line let name = "Zoro";
is actually run.
Now compare this to var
:
function sayHi() {
console.log(name); // undefined
var name = "Zoro";
}
sayHi();
This time you get undefined
, because var
is hoisted and initialised with undefined
. Not smarter, just sneakier.
The TDZ is Actually a Good Thing
Yes, I said it. The TDZ is your friend. It exists to prevent weird bugs and enforce better coding practices. If a variable is in the TDZ, it means:
“This thing has not been safely initialised yet, don't touch it.”
Without the TDZ, it would be a lot easier to write confusing or broken code. So, while it may feel like JavaScript is being overly strict, it’s actually trying to help you out.
Hoisting interview questions
1. Why doesn’t this function run as expected?
console.log(foo());
var foo = function () {
return "done";
};
Output:TypeError: foo is not a function
What’s wrong here?
The var foo
is hoisted, but its value is set to undefined
at the top. So when foo()
is called, it's actually calling undefined()
.
How to fix it?
Use a function declaration instead of a function expression if you need to call the function before it's defined:
function foo() {
return "done";
}
console.log(foo());
Or, move the function expression below the console.log
if you're using modern patterns like const
or let
.
2. What’s happening here with let
and shadowing?
let a = 10;
function test() {
console.log(a);
let a = 20;
}
test();
Output:ReferenceError
What’s wrong here?
At first glance, you might think a
will be 10
. But inside the function, let a
creates a new a
in the block scope, and it’s in the Temporal Dead Zone when console.log(a)
runs.
How to fix it?
If you want to access the outer a
, don't redeclare it with let
inside the function:
let a = 10;
function test() {
console.log(a); // 10
}
test();
If you need a new a
, define it after you've logged the outer one:
function test() {
console.log(a); // 10
let aNew = 20;
}
3. What’s going on with these var
declarations?
var count = 5;
(function () {
if (false) {
var count = 10;
}
console.log(count);
})();
Output:undefined
What’s wrong here?
You might expect count
to be 5
, but the inner var count
is hoisted to the top of the IIFE and initialised as undefined
, even though the if
block never runs.
So inside the function, count
is undefined
due to hoisting, not 5
from the outer scope.
How to fix it?
Use let
or const
to keep block scoping:
var count = 5;
(function () {
if (false) {
let count = 10;
}
console.log(count); // 5
})();
Or rename variables to avoid this subtle collision.
4. Why is this loop printing unexpected values?
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
Output:3
3
3
What’s wrong here?
The var i
is function-scoped, not block-scoped. So all three callbacks share the same i
and print its final value after the loop ends.
How to fix it?
Use let
for block scoping:
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
Or use an IIFE to capture the current i
:
for (var i = 0; i < 3; i++) {
(function (j) {
setTimeout(() => console.log(j), 100);
})(i);
}
5. Why does this throw when you access foo
inside bar
?
function foo() {
console.log("outer foo");
}
function bar() {
console.log(foo);
function foo() {
console.log("inner foo");
}
}
bar();
Output:[Function: foo]
Wait... it doesn’t throw?
Actually no, but here's the twist, this often catches people off-guard.
What’s really happening?
The function foo
inside bar
is hoisted to the top of bar
's scope. So when console.log(foo)
runs, it’s actually logging the function itself, not calling the outer foo
.
Follow-up:
Change console.log(foo)
to foo()
what happens?
function bar() {
foo(); // this calls the inner foo, not the outer one
function foo() {
console.log("inner foo");
}
}
bar();
The output is "inner foo"
because the inner declaration overshadows the outer one.
Fix (if you want the outer one):
Rename the inner function or avoid redeclaring functions with the same name.
6. Why does this function behave differently based on where it’s declared?
"use strict";
if (true) {
function test() {
console.log("block");
}
}
test();
Output:
In some environments: ReferenceError
Why is this tricky?
Function declarations inside blocks are a grey area in JavaScript. In strict mode, function declarations are not block-scoped in older engines, and behaviour can vary between browsers.
In modern JavaScript, this should throw a ReferenceError, because test
is scoped only inside the if
block.
How to fix it?
Use function expressions or const
if declaring inside a block:
if (true) {
const test = () => console.log("block");
test();
}
Or, declare it outside the block if it needs wider visibility.
That’s a Wrap
I know you are having a blast but the show must end here, I have to feed my pet (virtual pet 🫠). On the bright side you can now tackle any hoisting and scoping related interview questions like a pro. Next time we meet, I’ll let you in on another JavaScript mystery. Spoiler alert: PROMISES!
Subscribe to my newsletter
Read articles from Manik Agnish directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Manik Agnish
Manik Agnish
I am a front-end software developer and user experience designer based in India. I have worked extensively on creating inspiring web experiences and wish to share my knowledge through these blogs.