
Back when Netscape was in a frantic rush to shove a scripting language into their browser, they told a very smart guy named Brendan Eich to build one. The marketing department, in its infinite wisdom, had decided it needed to be called “JavaScript” and look like Java. So we got curly braces and semicolons. But underneath that Java-like veneer, it was a different beast entirely, a Lisp-like creature trying to break free. This strange parentage resulted in some… let’s call them quirky design choices. For nearly two decades, the only way you could declare a variable was with the keyword var. And var was, to be polite, a source of endless confusion and bugs.
You see, if you came from a sane language like C++, C#, or Java, you understood block scope. You declare a variable inside a for loop’s curly braces, and that variable ceases to exist outside those curly braces. It’s a simple, logical containment system. JavaScript, with var, decided to do something completely different. It doesn’t have block scope. It has function scope. This means a variable declared anywhere inside a function is available everywhere inside that function, from the first line to the last.
This single design decision was the architect of a whole category of baffling bugs. It gave us the classic interview brain-teaser that has tormented junior developers for a generation. Take a look at this code:
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
A rational programmer would look at this and expect it to print 0, 1, 2, 3, and 4, each after a one-second delay. But that’s not what happens. What you actually get in your console is 5, printed five times. Why? Because there is only one variable i for the entire scope. The loop runs to completion in a few milliseconds, leaving i with its final value of 5. A full second later, when the first timeout callback finally executes, it looks for i. It finds it, and its value is 5. The same thing happens for the next four callbacks. They all share the same, single i from the parent scope.
As if that wasn’t enough fun, var also introduced us to the concept of “hoisting.” This means that no matter where you declare a variable with var inside a function, the declaration is silently moved to the very top of that function by the JavaScript engine. The initialization, however, stays where you put it. This leads to code that looks like it should fail but instead produces undefined.
console.log(myVar); // Outputs: undefined var myVar = "Hello, world!"; console.log(myVar); // Outputs: "Hello, world!"
You’re using myVar before it’s declared! This should be an error. But it’s not. Because of hoisting, the code you wrote is actually interpreted by the engine as if you had written this:
var myVar; // Declaration is hoisted to the top console.log(myVar); myVar = "Hello, world!"; // Initialization stays here console.log(myVar);
To deal with the scope problem, clever developers came up with a pattern that was as ugly as it was necessary: the Immediately Invoked Function Expression (IIFE). To fix our broken loop, we had to wrap the contents in a new function and immediately call it, just to create a new function scope that could trap the value of i for each iteration.
for (var i = 0; i < 5; i++) {
(function(j) {
setTimeout(function() {
console.log(j);
}, 1000);
})(i);
}
This worked. It was also a hideous kludge. We were manufacturing scope with extra functions because the language didn’t give us the tools we needed. This was the state of JavaScript development: a minefield of scope traps and hoisting weirdness, patched over with convoluted patterns. It was clear this madness couldn’t last.
MOSISO Compatible with MacBook Neo Case 13 inch 2026 Release Model A3404 with A18 Pro Chip, 4 in 1 Kit Precision Fit Crack & Scratch Resistant Protective Hard Shell Case Cover, Crystal Clear
$9.99 (as of June 2, 2026 22:39 GMT +00:00 - More infoProduct prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on [relevant Amazon Site(s), as applicable] at the time of purchase will apply to the purchase of this product.)How two new keywords brought sanity to JavaScript
Then, in 2015, the clouds parted, angels sang, and the TC39 committee, the group of people responsible for standardizing JavaScript, handed down the tablets of ECMAScript 2015 (or ES6, as it’s commonly known). And on those tablets were two new keywords for declaring variables: let and const. Suddenly, JavaScript developers could write code that behaved the way programmers from every other language expected it to.
let is, for all intents and purposes, the new var. It’s the var we should have had all along. It has proper block scope. This means a variable declared with let inside a set of curly braces-be it a loop, an if statement, or just a standalone block-only exists inside those curly braces. It’s that simple. It’s that logical.
Let’s revisit that disastrous for loop that caused so much grief. If we just swap var for let, something magical happens:
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
Run this code, and what do you get? You get 0, 1, 2, 3, 4. It just works. The heavens don’t open. You don’t have to contort your code into an unreadable mess with an IIFE. The reason is that with let, a new, separate binding for the variable i is created for each and every iteration of the loop. The first callback closes over the i that was 0. The second callback closes over the completely separate i that was 1. And so on. Block scope fixes the problem cleanly and elegantly.
What about hoisting? let puts a stop to that nonsense, too. While technically let declarations are still “hoisted,” they are not initialized. Trying to access a let variable before its declaration in the code results in a ReferenceError. This area between the start of the block and the variable’s declaration is called the “Temporal Dead Zone” (TDZ). It’s a fancy name for a simple concept: you can’t use a variable before you’ve declared it. What a concept!
console.log(mySaneVar); // Throws ReferenceError! let mySaneVar = "This is better.";
This is a massive improvement. Instead of your code failing silently with an undefined value that you have to debug later, it just crashes immediately, pointing you right to the line where you made a mistake. This is a good thing. Fail fast, fail loud.
The standards committee didn’t stop there. They also gave us const. The const keyword behaves exactly like let in terms of scope-it’s block-scoped and has a TDZ. But it adds one more rule: once you assign a value to a const variable, you can never reassign it. It’s a constant.
const SECONDS_IN_MINUTE = 60; SECONDS_IN_MINUTE = 61; // Throws TypeError: Assignment to constant variable.
This is enormously useful. It prevents you or another developer from accidentally changing a value that was never meant to be changed. It’s a form of documentation right in the code, signaling intent. However, there’s a critical gotcha here that trips up a lot of people. const does not make the value itself immutable. It only makes the *binding*-the connection between the variable name and its value-constant. If the variable holds a reference to an object or an array, you can still change the contents of that object or array.
const USER_SETTINGS = {
theme: "dark",
notifications: true
};
// This is perfectly valid!
USER_SETTINGS.theme = "light";
// But this will fail. You can't reassign the variable.
USER_SETTINGS = { theme: "light" }; // TypeError!
You can mutate the object that USER_SETTINGS points to, but you can’t make USER_SETTINGS point to a completely different object. This is a subtle but vital distinction. It means const is perfect for declaring objects and arrays whose contents you expect to change, but whose reference you never want to overwrite.
So what’s a smart programmer to do now
So now we have these shiny new tools, let and const, and the rusty old var that we’re supposed to put in a museum. The question is, what’s the game plan? What’s the rule? You can’t just go around using whatever keyword feels right that day. That’s chaos. Programmers need rules. Simple, easy-to-follow rules that eliminate thinking and prevent bugs.
Here is the rule. It’s so simple you can write it on a sticky note and put it on your monitor.
- Use
constby default. - Use
letonly when you know you need to reassign the variable. - Never use
var.
That’s it. Let’s break this down. Why const by default? Because most of your variables shouldn’t be reassigned. Think about it. You get a user object from an API. You get a reference to a DOM element. You define a configuration value. These things are assigned once, and then you just use them. By making them const, you are making a promise to the next programmer (who is probably you, six months from now, with no memory of writing this code) that this variable’s binding will not change. It’s a guarantee. It reduces the number of moving parts you have to keep in your head.
When you see let, it should be a flag. It should draw your eye. It tells you, “Pay attention! This variable’s value is going to be changed somewhere within this block of code.” This is incredibly valuable information. It’s typically used for things like loop counters or variables that track state within a function, like let total = 0; that you then add to. By using let sparingly, you make its appearance more meaningful.
Let’s look at a practical example. Imagine we’re fetching a list of products and calculating the total price.
const API_ENDPOINT = "https://api.example.com/products";
const shippingCost = 10.50;
async function calculateTotal() {
const response = await fetch(API_ENDPOINT);
const products = await response.json();
let subtotal = 0;
for (const product of products) {
subtotal += product.price;
}
const totalWithTax = subtotal * 1.20;
const finalPrice = totalWithTax + shippingCost;
console.log(Your final price is: ${finalPrice}); }
See the discipline here? API_ENDPOINT and shippingCost are fundamental constants that will never change. They are const. The response from the fetch and the products array we get back are also assigned only once. We might mutate the contents of the products array later, but we’re not going to reassign the products variable itself to a new array, so it’s const. The subtotal is the only thing that needs to be reassigned-it’s a running total that we update in the loop. So it, and only it, gets to be let. The product in the for...of loop is also a const because a new binding is created for each iteration; it’s never reassigned within the loop’s body. totalWithTax and finalPrice are calculated and assigned once. const. This code is clean, predictable, and easy to reason about.
So what about var? Should you ever use it? The simple answer is no. Don’t use it in new code. The only reason you need to understand how var works is so you can read and maintain the mountains of legacy code written before 2015. For any new project, any new file, any new line of code you write, pretend it doesn’t exist. There are some incredibly obscure edge cases where var‘s function-scoping behavior might be technically useful, but they are so rare and generally indicative of a poor design that they aren’t worth considering. Sticking to let and const will make your code more robust, more readable, and save you from entire categories of bugs that plagued JavaScript developers for two decades. It’s one of the biggest free lunches the language has ever given us.
