Block Scope
In the previous lesson, you learned about global scope and function scope.
Now you will learn another important type of scope: block scope.
Block scope is one of the main reasons modern JavaScript code uses let and const instead of var.
What Is a Block?
A block is code wrapped in curly braces:
{
// This is a block
}You see blocks in many places:
if (isLoggedIn) {
console.log("Welcome back!");
}for (let i = 0; i < 3; i++) {
console.log(i);
}while (count < 5) {
count++;
}Blocks appear in:
ifandelsestatementsforloopswhileloopsswitchstatementstry...catchstatements- standalone
{}blocks
Block scope means a variable only exists inside the block where it was declared.
Block Scope With let
Variables declared with let are block-scoped.
if (true) {
let message = "Inside the block";
console.log(message); // Inside the block
}
console.log(message); // ReferenceErrorThe variable message exists inside the if block.
Once the block ends, message is no longer available.
This keeps temporary variables contained.
Block Scope With const
Variables declared with const are also block-scoped.
if (true) {
const status = "active";
console.log(status); // active
}
console.log(status); // ReferenceErrorThe variable status only exists between the { and } of the if block.
This is exactly what you usually want.
If a value is only needed inside a block, it should not leak into the rest of the program.
The var Trap
The older var keyword does not follow block scope.
var is function-scoped.
That means a var declared inside an if block can still be accessed outside that block.
if (true) {
var leakedMessage = "I escaped the block";
}
console.log(leakedMessage); // I escaped the blockThis can be surprising.
The variable looks like it belongs to the if block, but it does not.
Because var ignores block scope, it can leak into a wider scope than you intended.
var Inside a Function
Inside a function, var is scoped to the whole function.
function showStatus(isActive) {
if (isActive) {
var status = "active";
}
console.log(status);
}
showStatus(true); // activeThe status variable is declared inside the if block.
But because it uses var, it is available throughout the entire showStatus function.
This can also produce confusing results:
function showStatus(isActive) {
if (isActive) {
var status = "active";
}
console.log(status);
}
showStatus(false); // undefinedWhen isActive is false, the assignment never happens.
But the var declaration still belongs to the function scope, so status exists with the value undefined.
You will learn more about this behavior in the hoisting lesson.
let and const Stay Inside the Block
Now compare the same idea with let:
function showStatus(isActive) {
if (isActive) {
let status = "active";
console.log(status); // active
}
console.log(status); // ReferenceError
}
showStatus(true);The variable status only exists inside the if block.
The same is true with const:
function showStatus(isActive) {
if (isActive) {
const status = "active";
console.log(status); // active
}
console.log(status); // ReferenceError
}
showStatus(true);This makes the code safer.
The variable cannot accidentally be used outside the block where it belongs.
Block Scope in if...else
Each block has its own scope.
const score = 85;
if (score >= 50) {
const result = "Pass";
console.log(result); // Pass
} else {
const result = "Fail";
console.log(result);
}Both blocks declare a variable named result.
This is allowed because each result belongs to a different block.
But outside the if...else, neither variable exists:
const score = 85;
if (score >= 50) {
const result = "Pass";
} else {
const result = "Fail";
}
console.log(result); // ReferenceErrorIf you need the value after the block, declare it in the outer scope:
const score = 85;
let result;
if (score >= 50) {
result = "Pass";
} else {
result = "Fail";
}
console.log(result); // PassOr, for simple choices, use an expression:
const score = 85;
const result = score >= 50 ? "Pass" : "Fail";
console.log(result); // PassBlock Scope in Loops
Block scope is especially useful in loops.
for (let i = 0; i < 3; i++) {
console.log(i);
}
console.log(i); // ReferenceErrorThe loop counter i only exists inside the loop.
That is good.
The counter is a temporary variable. It should not pollute the outer scope.
With var, the loop counter leaks:
for (var i = 0; i < 3; i++) {
console.log(i);
}
console.log(i); // 3After the loop finishes, i is still available.
This is one of the classic problems with var.
Why let Is Better for Loop Counters
Use let for loop counters because the value changes on each iteration.
for (let index = 0; index < 3; index++) {
console.log(index);
}The variable index belongs to the loop.
It does not leak outside:
console.log(index); // ReferenceErrorThis keeps your outer scope clean.
It also prevents accidental reuse:
for (let i = 0; i < 3; i++) {
console.log(i);
}
for (let i = 0; i < 2; i++) {
console.log(i);
}Both loops can safely use i.
Each loop has its own block-scoped i.
Blocks Can Be Standalone
You can create a block without an if, loop, or function.
const value = 10;
{
const value = 20;
console.log(value); // 20
}
console.log(value); // 10The inner value only exists inside the standalone block.
The outer value is unchanged.
Standalone blocks are not used every day, but they can be useful when you want a temporary private scope.
Shadowing With Block Scope
When an inner scope declares a variable with the same name as an outer scope, the inner variable shadows the outer one.
const mode = "light";
if (true) {
const mode = "dark";
console.log(mode); // dark
}
console.log(mode); // lightInside the block, JavaScript uses the closest mode.
Outside the block, JavaScript uses the outer mode.
Shadowing is valid, but it can become confusing.
Use it only when the meaning is clear.
Block Scope Does Not Replace Function Scope
Block scope and function scope both matter.
function createMessage(isAdmin) {
const prefix = "User type:";
if (isAdmin) {
const role = "admin";
return `${prefix} ${role}`;
}
const role = "viewer";
return `${prefix} ${role}`;
}
console.log(createMessage(true)); // User type: admin
console.log(createMessage(false)); // User type: viewerHere:
prefixis function-scoped because it is declared in the function body- each
roleis block-scoped because it is declared inside a block
The inner block can access prefix from the outer function scope.
But the function scope cannot access a role declared inside an if block after that block ends.
const and Objects Inside Blocks
Remember that const prevents reassignment.
It does not make objects immutable.
if (true) {
const user = {
name: "Alice",
};
user.name = "Ava";
console.log(user.name); // Ava
}The user variable is block-scoped.
Outside the block, it is not available:
console.log(user); // ReferenceErrorBut while inside the block, the object it points to can still be changed.
This connects back to what you learned about const and object mutation.
catch Blocks
catch blocks also create block scope.
try {
JSON.parse("{ bad json }");
} catch (error) {
console.log(error.message);
}
console.log(error); // ReferenceErrorThe error variable only exists inside the catch block.
This keeps error-handling variables from leaking into the rest of the code.
switch and Blocks
A switch statement uses braces, but its cases can be tricky because all cases share the same block unless you add extra braces.
This can cause problems:
const role = "admin";
switch (role) {
case "admin":
const message = "Admin user";
console.log(message);
break;
case "viewer":
const message = "Viewer user";
console.log(message);
break;
}This code can fail because both message declarations are in the same switch block.
You can fix it by adding braces around each case:
const role = "admin";
switch (role) {
case "admin": {
const message = "Admin user";
console.log(message);
break;
}
case "viewer": {
const message = "Viewer user";
console.log(message);
break;
}
}Now each case has its own block scope.
Why Modern JavaScript Avoids var
Modern JavaScript almost always uses const and let.
The main reasons are:
constandletare block-scopedvaris only function-scopedvarcan leak out of blocksvarcan make hoisting behavior confusing- block scope makes code easier to reason about
Default to const.
Use let when the variable must be reassigned.
Avoid var in modern code.
Best Practices
Declare variables as close as possible to where they are used:
if (cartTotal > 100) {
const discount = cartTotal * 0.1;
console.log(discount);
}Use const by default:
const maxAttempts = 3;Use let when the value changes:
let attempts = 0;
while (attempts < 3) {
attempts++;
}Avoid var:
// Avoid this in modern JavaScript
var message = "Hello";If you need a value outside a block, declare it outside the block intentionally:
let message;
if (isLoggedIn) {
message = "Welcome back";
} else {
message = "Please sign in";
}
console.log(message);Do not declare a value inside a block and expect it to exist outside.
Common Mistakes
Mistake 1: Trying to Use let Outside Its Block
if (true) {
let message = "Hello";
}
console.log(message); // ReferenceErrorThe variable message only exists inside the block.
Mistake 2: Expecting var to Stay Inside a Block
if (true) {
var message = "Hello";
}
console.log(message); // Hellovar does not respect block scope.
Use let or const instead.
Mistake 3: Declaring a Result Inside if...else and Using It Later
if (score >= 50) {
const result = "Pass";
} else {
const result = "Fail";
}
console.log(result); // ReferenceErrorDeclare the variable outside the block if it must be used later:
let result;
if (score >= 50) {
result = "Pass";
} else {
result = "Fail";
}
console.log(result);Mistake 4: Forgetting That switch Cases Share Scope
switch (role) {
case "admin":
const label = "Admin";
break;
case "viewer":
const label = "Viewer";
break;
}Add braces to create separate block scopes:
switch (role) {
case "admin": {
const label = "Admin";
break;
}
case "viewer": {
const label = "Viewer";
break;
}
}Quick Check
What happens here?
if (true) {
const message = "Inside";
}
console.log(message);It throws a ReferenceError.
message is block-scoped.
What does this log?
if (true) {
var message = "Inside";
}
console.log(message);It logs:
Insidevar does not stay inside the block.
What does this log?
const value = 1;
{
const value = 2;
console.log(value);
}
console.log(value);It logs:
2
1The block has its own value, and the outer value is unchanged.
Summary
Block scope controls variables declared inside curly braces.
- A block is code inside
{}. letandconstare block-scoped.varis not block-scoped; it is function-scoped.- Loop counters declared with
letstay inside the loop. - Standalone blocks can create temporary private scopes.
switchcases may need extra braces to create separate scopes.- Use
constby default,letwhen reassignment is needed, and avoidvarin modern JavaScript.