AlgoInsight

Free lesson · JavaScript Basic to Advance

Block vs Function Scope in Practice

Easy5 min read

Two Different Scoping Rules in the Same Language

JavaScript has two scoping rules running side-by-side, and they don't agree with each other. Which rule applies depends on which keyword you used to declare the variable.

function compareScoping() {
  if (true) {
    var functionScoped = "I leak out";
    let blockScoped = "I stay in";
  }

  console.log(functionScoped);   // ✅ "I leak out"
  // console.log(blockScoped);   // ❌ ReferenceError
}

compareScoping();

The var declared inside the if block is visible outside the block, because var doesn't care about block boundaries — only function boundaries. The let declared in the same block is invisible outside it, because let respects the block.

This difference is the single biggest reason ES6 introduced let and const. var's function-scoped behavior produced bugs for two decades. The fix was a new keyword with a saner rule.

Interactive animation: A branch that never ran, and a name that exists anyway - var leaks out of the if, let does not.

Why Function Scope Causes Problems

Function scope sounds reasonable in the abstract. Variables stay inside the function they're declared in. What could go wrong?

The problem is that "inside the function" is a much wider area than developers expect. Look at this loop:

function findUser(users, targetName) {
  for (var i = 0; i < users.length; i++) {
    var current = users[i];
    if (current.name === targetName) {
      return current;
    }
  }

  console.log(i);         // still accessible — 'i' leaked out of the loop
  console.log(current);   // still accessible — 'current' leaked too
}

findUser([{name: "Olena"}, {name: "Nikhil"}], "Nikhil");

i and current were intended to be loop variables — they should only exist while the loop is running. But because they're var, they're function-scoped. They leak out of the loop and are accessible throughout the rest of the function.

This isn't a bug here. The function returns before reaching those console.log lines (when it finds Nikhil). But imagine more complex code, where the loop variable is accidentally reused later in the function for something else. Bugs like that are real, and they happened constantly in pre-ES6 JavaScript.

let and const fix this by being block-scoped. A loop counter declared with let exists only inside the loop:

function findUserModern(users, targetName) {
  for (let i = 0; i < users.length; i++) {
    let current = users[i];
    if (current.name === targetName) {
      return current;
    }
  }

  // console.log(i);         // ❌ ReferenceError — 'i' doesn't exist here
  // console.log(current);   // ❌ ReferenceError — 'current' doesn't exist here
}

findUserModern([{name: "Olena"}, {name: "Nikhil"}], "Olena");

The variables are gone the moment the loop ends. Trying to use them outside throws an error — exactly what you want.

Interactive animation: What survives the loop - a var counter outlives it, a let counter is gone the moment it ends.

The Block-Scope Boundaries

Block scope is defined by curly braces. Any pair of curly braces creates a block. This includes the ones you'd expect (if, else, for, while) and also ones you might not (plain {} blocks used for grouping).

{
  let groupedVariable = "I exist only in this plain block";
  console.log(groupedVariable);
}

// console.log(groupedVariable);   // ❌ ReferenceError

if (true) {
  let conditionalVariable = "I exist only in this if block";
  console.log(conditionalVariable);
}

for (let counter = 0; counter < 3; counter++) {
  let loopBodyVariable = `iteration ${counter}`;
  console.log(loopBodyVariable);
}

Each set of curly braces is a separate block, with its own scope. Variables declared with let or const inside a block are invisible from outside.

graph TD
    A[function example] --> B["{ outer block }"]
    B --> C["{ if block }"]
    B --> D["{ for block }"]
    D --> E[loop body]

    F[var declared in any of these] -.->|leaks all the way up to| A
    G[let/const declared in any of these] -.->|stays in its own block| H[the immediate enclosing block]

    style A fill:#7c3aed,color:#fff
    style F fill:#dc2626,color:#fff
    style G fill:#10b981,color:#fff
This diagram shows the difference between var and let in nested blocks. A function can contain multiple blocks: an outer block, an if block, a for block, and a loop body. When you declare a variable with var anywhere in these blocks, it leaks all the way up to the function level. When you declare a variable with let or const, it stays within its immediate enclosing block.

The Classic for-Loop Bug

The most famous bug caused by var's function-scoping involves loops and asynchronous code. Let me show you the bug, then the fix.

// Buggy version with var
console.log("Buggy version:");
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(`i is ${i}`), 10);
}

If you ran that, all three logs would print "i is 3". Not "i is 0", "i is 1", "i is 2" as you might expect. The reason: by the time the timer callbacks run (10 milliseconds later), the loop has finished and i has reached 3. Because var is function-scoped, all three callbacks share the same i, which now equals 3.

Now the fix:

// Fixed version with let
console.log("Fixed version:");
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(`i is ${i}`), 10);
}

With let, each iteration of the loop creates a new i variable, scoped to that iteration's block. The three callbacks each capture their own i — 0, 1, and 2 respectively.

This bug — and the fix — is one of the most-asked JavaScript interview questions. Interviewers love it because it tests three concepts at once: scope, closures, and the asynchronous model. We cover it again in detail in Module 6 (closures) and Module 9 (async).

Interactive animation: What each timeout captured - one shared var i that ends at 3, against a fresh let i per iteration.

A Mental Model

When you read JavaScript code, train yourself to scan for variable declarations and ask:

This is the practical skill for reading JavaScript fluently. You're tracking the lifetime of each variable as you read through the code, knowing exactly when each one comes into existence and when each one disappears.

For new code you write, the rule is simpler: use let and const exclusively. You'll never have to think about function scope vs block scope, because you'll only be using block scope. The whole category of var-related bugs is closed off.

Common Pitfalls

Interactive animation: Every case of a switch shares one block - wrapping each case in braces gives it its own scope.

Warning: The block-scope traps.

Assuming var is block-scoped. Beginners often assume that any variable declared inside curly braces is local to those braces. This is true for let and const, but not for var. Code that "works" with var may have variables silently leaking in ways the author didn't intend.

Forgetting that switch cases share scope. A case block inside a switch does not create its own block scope unless you wrap it in { }. Declaring let x in one case and trying to use it in another can cause unexpected ReferenceErrors or accidental reuses.

Loop variables in async callbacks. If you have a loop with var and inside the loop you set up async callbacks (timers, promises, event handlers), all the callbacks will share the same loop variable. Switch to let to fix it.

Why This Matters in Interviews

Interview: The for-loop + var versus let question is one of the most common JavaScript interview questions, especially at the senior level. Interviewers will write code like the example above and ask "what does this print, and why?"

A senior-level answer mentions three things: (1) var is function-scoped, so all callbacks share one i; (2) the callbacks run asynchronously, after the loop has finished, when i already equals the end value; (3) let fixes this because it's block-scoped, so each iteration creates its own i.

Topic 3.6 — Interview Lens: Variables, Scope, and Hoisting — covers the full interview-prep treatment of these scope questions, including the variants interviewers use to probe whether you actually understand the mechanism.

Try It

Exercise: Predict the output of each block. Then run them.

// Block 1
for (var i = 0; i < 3; i++) {}
console.log(i);

// Block 2
for (let j = 0; j < 3; j++) {}
console.log(j);

// Block 3
{
  var k = 10;
}
console.log(k);

// Block 4
{
  let m = 20;
}
console.log(m);

Two of these print a value. Two throw a ReferenceError. Identify which is which and trace through the rule that determines each outcome. Once you can predict this pattern instantly, you've internalized the difference between function and block scope.

Back to JavaScript Basic to Advance