When to Use var or let or const

In Brief
Use const for a binding that will not be reassigned, and let when reassignment is part of the design. Reserve var mainly for deliberate work with legacy code or function‑scoped behaviour. A const binding prevents reassignment of the binding itself; it does not make the properties of an object or the items in an array immutable.
With the introduction of ES6 back in 2015, a lot of new major features were introduced to the JavaScript language. Two of those features are the let keyword, and the const keyword. They give us two new ways to declare variables in JavaScript ‑ but why would we need them? Isn't var good enough already?
As you might expect, these weren't changes without cause; var, let, and const all have their own specific use cases where they make the most sense. Let's take a look at a few of the differences between these keywords below, starting with the one we're probably all most familiar with: var.
The var Keyword
var declares a variable in its containing function or at the top level. An ordinary block, such as an if block, does not give var a separate scope. A top‑level declaration belongs to the module in an ES module; in a classic browser script, it can create a global binding and a property on the global object.
{
var foo = 'bar';
console.log(foo);
//=> 'bar'
}
console.log(foo);
//=> 'bar'This is where scope begins to matter. The block above does not hide foo; both calls can read the same variable.
A var declared inside a function is local to that function. The risk in this example is reusing a name within the same scope, not that every var becomes global.
For an example, let's take an old‑school for loop:
var i = 500;
for (var i = 0; i < 1000; i++) {}
console.log(i);
//=> 1000When run, the console will output 1000 here, rather than the 500 you might (reasonably) expect. This might not seem immediately like a huge issue ‑ especially in an abstract four‑line example ‑ but in a full web app it might have some undesired consequences; redeclaring variables and ignoring block scoping can be an issue, especially when using single‑letter variables for simple things like i to represent iteration indexes
Paying more attention to the scopes that your variables are accessible in, and being strict with them, can help you keep things neat when working on large, complex sites or web apps and cut down on that spaghetti code. This is where the real magic of let and const become apparent.
The let Keyword
let lets us reassign a variable but not redeclare the same name in the same lexical scope. Unlike var, it also gives an ordinary block its own binding:
let i = 500;
for (let i = 0; i < 1000; i++) {}
console.log(i);
//=> 500Here, the loop has its own i binding. Incrementing it does not change the outer i, so the final call logs 500. That distinction comes from block scope; the outer variable does not need to be global.
However, there is another key benefit to bear in mind here, too; let prevents redeclaration.
var foo = 'bar';
var foo = 'foo';
console.log(foo);
//=> 'foo'The var example logs "foo": redeclaring that name in the same scope is allowed. By contrast, put let bar = 1; let bar = 2; in a separate script and it fails during parsing with a SyntaxError. None of that script runs, including any earlier logging statements. A browser may report it as follows:
Uncaught SyntaxError: Identifier 'bar' has already been declaredThe duplicate declaration is rejected before execution. This checks whether the same binding name is declared twice in one scope; it does not enforce a variable's value type.
It should be said here: the other key difference between let and const (which I will talk about below) is that let can be reassigned a new value very simply:
let lorem = 'ipsum';
lorem = 'dolor';
console.log(lorem);
//=> 'dolor'Here, the console will log out dolor because although lorem was originally assigned to ipsum, it was subsequently reassigned to dolor.
So, let's finish up by taking a look at const.
The const Keyword
const declares a binding that cannot be reassigned. Like let, it is block‑scoped and cannot be redeclared in the same lexical scope. Redeclaration produces a SyntaxError; assigning a new value to an existing const binding produces a TypeError when that assignment runs:
const foo = 'bar';
foo = 'foo';
console.log(foo);
//=> will not runThis example reaches the assignment to foo, throws a TypeError and never reaches the following console.log(). That is a runtime failure, unlike the duplicate lexical declaration discussed above:
Uncaught TypeError: Assignment to constant variableThat restriction protects the binding, rather than enforcing a fixed JavaScript value type or making an object immutable.
It is ‑ however ‑ worth bearing in mind that although a const variable cannot be reassigned, some object properties and array data stored within a const can be. Ideally, though you should intentionally choose to use const in situations where you do not want or need the variable to change.
There is one more useful distinction, often described as hoisting. Before its declaration executes, a var binding is already initialised to undefined. A let or const binding exists in its scope but stays uninitialised until its declaration is evaluated. Reading it before then throws a ReferenceError; that period is called the temporal dead zone.
The Wrap‑up
var, let, and const all accomplish essentially the same job; creating and declaring variables. However, let and const accomplish this in a much more structured and responsible way, allowing for clearer code and easier management of that code, by limiting scopes and preventing redeclaration. There might be times when you want to affect the global scope by using var, but by and large, it makes much more sense to be mindful and intentional with your usage of scopes when working with variables, even if just for the sake of the developer who comes to the code after you...!