I’m running into confusing behavior when I use the Javascript let keyword instead of var or const in my code. Some variables don’t seem to be accessible where I expect them to be, and I’m also seeing errors related to block scope and redeclaration. Can someone explain in simple terms when I should choose let over var or const, and maybe show a few clear examples of best practices for using let in modern Javascript so I can clean up my code and avoid these issues
You are hitting two main things with let:
- Block scope
- Temporal Dead Zone
Quick breakdown.
- Scope differences
var
- Function scoped.
- If you declare var x inside an if, x leaks outside that if.
Example:
if (true) {
var a = 1
}
console.log(a) // 1
let
- Block scoped.
- Lives only inside the nearest { }.
if (true) {
let b = 2
}
console.log(b) // ReferenceError
Same for for loops:
for (var i = 0; i < 3; i++) {}
console.log(i) // 3
for (let j = 0; j < 3; j++) {}
console.log(j) // ReferenceError
If your code expects a var style leak outside blocks, let will break that.
- Temporal Dead Zone (TDZ)
With var:
console.log(x) // undefined, no error
var x = 10
With let:
console.log(y) // ReferenceError
let y = 10
The variable exists from the start of the scope, but you cannot use it before its declaration. That is the TDZ. You see those errors when you access a let variable before the line where you declared it.
- let vs const
They have the same scope and TDZ rules.
Difference is mutability.
let x = 1
x = 2 // ok
const y = 1
y = 2 // TypeError
For objects and arrays, const only locks the binding, not the content.
const obj = { a: 1 }
obj.a = 2 // ok
obj = {} // TypeError
- When to use what
Practical rule of thumb:
- Use const by default.
- Use let if you need to reassign.
- Avoid var in new code.
Examples of common bugs you are likely seeing:
a) Expecting access outside a block
if (cond) {
let msg = ‘hi’
}
doSomething(msg) // ReferenceError
Fix:
let msg
if (cond) {
msg = ‘hi’
}
doSomething(msg)
b) Using variable before declaration
console.log(total)
let total = 5
Fix: move declaration to the top of the scope.
let total = 5
console.log(total)
If you paste a small snippet of where it breaks, you will see it is either scope of a block or TDZ every time.
One more angle on this, building on what @boswandelaar already laid out nicely.
What usually bites people with let/const vs var is how your mental model of a “scope” is wrong, not the keywords themselves.
You’re probably thinking “scope = file or function.”
JavaScript is more like: “scope = function + every pair of {} created by control structures.”
So things like this:
if (flag) {
let value = 42
doSomething(value)
}
useValue(value) // boom
Your brain: “I just defined value above in the same function, why can’t I use it?”
JS: “Nope, that value only lives inside the if {} block.”
If you intend to use it later, declare the variable in the outer scope, then assign inside the block:
let value
if (flag) {
value = 42
}
useValue(value) // value is either 42 or undefined
That pattern is the “fix” in like 70% of the confusing cases.
The other annoying part: mixing var and let in the same function. Example:
console.log(count)
let count = 10
var foo = 1
What a lot of folks expect:
countbehaves likefoo, so it logsundefined
What actually happens:
countis in the temporal dead zone soReferenceErrorfoois hoisted and initialized toundefined
If your codebase still has a lot of var and you drop in a single let, you get a weird hybrid of behaviors. Personally, I’d say either:
- Go all in: replace
vareverywhere withlet/constin that file, or - Stick with
varin that legacy function until you refactor it entirely.
I slightly disagree with the idea of “just always use const by default” without thinking. It’s a decent rule of thumb, but it can make code noisier when you’re doing stepwise refinement:
let result
if (cond) {
result = computeA()
} else {
result = computeB()
}
return result
For this sort of pattern, forcing const everywhere either leads to unnecessary extra variables or ternaries that hurt readability. So I’d say:
- Use
constfor values that genuinely don’t change conceptually - Use
letwhen the value naturally evolves over time in that function - Use
varonly when:- You absolutely must rely on function scope instead of block scope, or
- You’re stuck in older patterns and can’t refactor yet
One more subtle trap that might match what you’re seeing:
function example() {
console.log(a) // ReferenceError if a is let later in same scope
{
let a = 10
}
}
Some folks expect “the inner block doesn’t matter, I’m before it,” but that inner let a shadows any outer a for the entire scope, and the TDZ kicks in until the declaration line. So even if you later decide to add a let inside a block, it can break earlier console.log(a) that used to refer to some outer value. That feels super random when it happens during refactors.
If you want to track this stuff down in your actual code:
- Search for the variable name
- Check if it’s declared with
let/constinside a smaller block than where you’re trying to read it - Or if you’re touching it anywhere above its declaration line in the same function / block
Every “weird” let bug I’ve debugged in other people’s code has fallen into one of these buckets:
- Variable used outside its block
- Variable used before declaration because of TDZ
- Shadowing: inner
lethiding an outervaror parameter - Legacy
varassumptions lingering in a partially updated function
If you paste a specific snippet that’s failing, you can usually circle the {} and the declaration in 10 seconds and see exactly which of those 4 it is.