Skip to Content
Course content

99: Let, Const, and Block Scoping Revisited

Click on the "Edit" button in the top corner of the screen to edit your slide content.

We've touched on let and const before, but as you start building larger applications, the subtle differences between block scoping and function scoping start to cause bugs that are a nightmare to debug. I've seen senior devs spend an hour chasing a bug that was solved by changing a single keyword.

Take a look at this snippet. I'm trying to create a simple notification system that logs a series of messages with a slight delay between each one.

const messages = ['Loading...', 'Fetching data...', 'Almost there...', 'Done!'];

for (var i = 0; i < messages.length; i++) {
    setTimeout(() => {
        console.log(messages[i]);
    }, i * 1000);
}

// Expected: "Loading...", then "Fetching data...", etc.
// Actual: undefined, undefined, undefined, undefined (or an error)

The ghost of function scoping

If you ran this, you'd notice it doesn't print the messages in order. In fact, it likely prints undefined four times. Why? Because var does not care about blocks (like the for loop). It only cares about functions.

By the time the first setTimeout callback actually executes (after 0 milliseconds), the loop has already finished running. Because var i was hoisted to the top of the scope, there is only one i shared by every single timer. By the time the timers fire, i has already been incremented to 4. Since messages[4] doesn't exist, you get undefined.

Binding variables to the block

The fix is deceptively simple: change var to let.

const messages = ['Loading...', 'Fetching data...', 'Almost there...', 'Done!'];

for (let i = 0; i < messages.length; i++) {
    setTimeout(() => {
        console.log(messages[i]);
    }, i * 1000);
}

Now it works perfectly. When you use let in a for loop header, JavaScript does something clever: it creates a new binding for i for every single iteration of the loop. Each setTimeout callback "closes over" its own unique version of i that never changes. I always tell people: unless you have a very specific, legacy reason to use var, just stop using it. There is almost no scenario in modern JS where var is the better choice.

Choosing between Let and Const in logic blocks

Once you've moved away from var, you're left with a choice: let or const? I tend to be aggressive with const. If a variable doesn't need to be reassigned, make it a constant.

Consider this pattern. I'm calculating a discounted price based on a user's membership status:

const calculatePrice = (price, isMember) => {
    const discount = 0.1; 
    let finalPrice = price;

    if (isMember) {
        const memberBonus = 0.05;
        finalPrice = price * (1 - (discount + memberBonus));
    } else {
        finalPrice = price * (1 - discount);
    }

    return finalPrice;
};

Notice that memberBonus only exists inside the if block. If I tried to access it outside that block, the engine would throw a ReferenceError. This is a good thing. It keeps the "mental surface area" of your function small. You don't have to wonder where memberBonus came from when you're looking at the return statement because it's physically impossible for it to be there.

A quick rule of thumb I use: Start with const. If you realize you need to change the value (like finalPrice above), change it to let. If you find yourself using let everywhere, you might be mutating state more than you need to, which is usually where the most elusive bugs hide.




📋 Practical Task

Fixing the User Permission Leak

You are working on a permissions system. The following code is intended to check if a user has "Admin" or "Editor" rights to access a specific folder. However, there is a scoping bug causing the accessLevel to be handled incorrectly across different blocks.

The Bug: The code uses var, which allows accessLevel to leak and be overwritten in ways the developer didn't intend. Additionally, some variables that should be constant are declared with let.

Your Goal:

  • Refactor the code to use let and const appropriately.
  • Ensure that the temporaryOverride variable is strictly block-scoped to the if statement so it cannot be accessed by the final console.log.
  • Ensure the final output correctly identifies the user as "Admin" based on the logic.
const user = {
    role: 'Admin',
    id: 42
};

var accessLevel = 'Guest';

if (user.role === 'Admin') {
    var accessLevel = 'Full Access';
    var temporaryOverride = 'SuperUser_Mode'; 
    console.log('Admin access granted.');
} else if (user.role === 'Editor') {
    var accessLevel = 'Limited Access';
}

// This should print "Full Access", 
// but temporaryOverride should NOT be accessible here.
console.log('Final Access Level: ' + accessLevel);
console.log('Override Status: ' + temporaryOverride); 
Rating
0 0

There are no comments for now.

to be the first to leave a comment.