Back to issues
JAVASCRIPT

Function Scope in JavaScript: Why var Still Causes Bugs in 2026

Understand how function scope works with var, why hoisting exists, the classic loop-with-setTimeout problem, and why let and const fix everything.

By Thiago Saraiva7 MIN

Mental model: Think of var as a post-it stuck on the wall of the entire office, anyone walking by can grab it, move it, overwrite it. let and const are post-its on your own desk, inside your cubicle (the block). Step out of the cubicle, the post-it is gone. Same paper, radically different blast radius.

You declare a variable inside an if, and somehow it's accessible outside. You log a variable before declaring it, and instead of an error, you get undefined. If you've been through this, you've met JavaScript's function scope, and you probably didn't like it.

Here's the thing: var isn't broken. It's just old. Brendan Eich designed JavaScript in 10 days in May 1995, and function scope was the simplest thing that could possibly work. Nobody imagined we'd be writing million-line SPAs on top of it 30 years later. let and const arrived in 2015 (ES6) to fix what two decades of real-world pain had exposed.

What Is Function Scope

Variables declared with var are accessible anywhere within the function, regardless of the block where they were declared:

var doesn't respect blocks (if, for, while). It only respects functions. let and const respect blocks.

Put another way: var treats { } as decoration. let and const treat { } as walls.

Hoisting: Why This Happens

JavaScript processes code in two phases:

  1. Creation Phase: var declarations are moved to the top of the function and initialized as undefined
  2. Execution Phase: assignments happen at the original position

With let, using a variable before declaration throws ReferenceError. This is the Temporal Dead Zone (TDZ), and it's much safer. TDZ is JavaScript's way of saying: "The variable exists, but you're not allowed to touch it yet. Wait your turn."

War Story: The jQuery Plugin That Leaked Everywhere

Around 2012, a colleague debugged a jQuery plugin that was silently corrupting event handler state in production. The culprit? A for (var i = 0; ...) loop inside a plugin method, where i leaked out of the loop and into a closure passed to .each(). Every element ended up bound to the terminal value of i instead of its own index. One missing pair of parentheses around an IIFE, eight months in production, hundreds of mis-wired click handlers. This is why IIFEs ((function(){ ... })()) became the signature move of pre-ES6 JavaScript: wrapping code in a function was the only way to get block-like isolation.

Quick Reference: var vs let vs const

Featurevarletconst
ScopeFunctionBlockBlock
HoistedYes (as undefined)Yes (but TDZ)Yes (but TDZ)
TDZNoYesYes
Redeclare in scopeAllowedSyntaxErrorSyntaxError
ReassignYesYesNo
Global object propYes (window.x)NoNo

The Classic Problem: Loop with var

Probably the most famous JavaScript interview question:

Why? Because var i exists only once in the function. When the callbacks fire (100ms later), the loop has finished and i is 3.

let creates a fresh binding per iteration. Each callback captures its own i. It's like handing every async task its own photocopy of the counter, instead of pointing them all at the same whiteboard that keeps getting erased.

Pitfalls That var Still Causes

Accidental redeclaration:

Global pollution: forgetting var (or let/const) creates a global variable:

Use 'use strict' to prevent this.

When You Might Still See var in 2026

You won't write var in new code, but you'll read it. Legacy codebases (anything older than ~2017), transpiled output targeting ES5 for ancient browsers, polyfills shipped as UMD bundles, and a surprising amount of WordPress plugin territory still lean on var. If you maintain any of these, understanding function scope isn't optional, it's survival gear.

FAQ

Does let get hoisted? Yes, technically. But it's hoisted into the Temporal Dead Zone, so accessing it before declaration throws ReferenceError. The practical effect: it behaves as if it weren't hoisted at all.

What exactly is the TDZ? The zone between entering a scope and the line where let/const is declared. Inside it, the binding exists but reading or writing throws. It's a feature, not a bug: it turns a silent undefined into a loud error.

Do IIFEs still make sense? Rarely. Their original job (creating a scope) is handled by { } with let/const, or by ES modules. You'll still see IIFEs in bundled output and in code that needs an immediately-invoked async context, like (async () => { await thing(); })().

Is var in a for loop worse than it looks? Yes. Beyond the setTimeout classic, var i survives the loop. If later code reads i, you get the terminal value with zero warning. With let, i is gone the moment the loop ends, exactly what you want.

Will my linter save me? Mostly. ESLint with no-var, prefer-const, and no-implicit-globals catches 95% of this. The other 5% is legacy code your linter was told to ignore, and bugs that only surface at runtime. Linters are seatbelts, not autopilot.

Key Takeaways

  • var has function scope: it ignores blocks and is subject to hoisting
  • let and const have block scope: more predictable and safer
  • The loop-with-setTimeout problem is caused by function scope
  • Modern rule: always use let and const. Avoid var.
  • Understanding function scope is still important for debugging legacy code and answering interview questions