I spent my first year writing JavaScript wondering why my variables kept acting weird in loops and conditionals.
Then I discovered let and half my debugging time disappeared overnight.
What you'll learn: How to use let properly and avoid the 3 most common variable scope bugs
Time needed: 10 minutes of reading, 5 minutes of practice
Difficulty: Beginner-friendly with real examples
You'll stop getting "undefined" when you expect actual values, and your code will be way more predictable.
Why I Had to Learn This the Hard Way
My setup:
- Writing JavaScript for client projects
- Constantly confused by variable behavior
- Wasting hours debugging scope issues
- Using
varfor everything (big mistake)
What didn't work:
- Just using
vareverywhere - created weird bugs - Googling "why is my variable undefined" 50 times a day
- Copy-pasting code without understanding scope
I kept hitting the same problems until I understood how let actually works.
The Problem: Variables That Act Weird
The main issue: var doesn't work like you think it does
My solution: Use let for block scope and cleaner code
Time this saves: Stop debugging mystery "undefined" errors
What's Wrong with 'var'?
Here's the exact bug that made me learn let:
// This code looks fine but breaks in weird ways
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log("Loop value:", i); // Always prints 3!
}, 100);
}
// My browser console showed:
// Loop value: 3
// Loop value: 3
// Loop value: 3
What this does: Creates one variable that all setTimeout callbacks share Expected output: Numbers 0, 1, 2 Actual output: Number 3 three times
Personal tip: "I wasted 2 hours on this exact bug in my first React project"
Step 1: Switch from 'var' to 'let' for Block Scope
The problem: var has function scope, not block scope
My solution: Use let for variables that should stay in their block
Time this saves: No more accidental variable sharing
// Old way with var (broken)
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log("var i:", i); // 3, 3, 3
}, 100);
}
// New way with let (works correctly)
for (let j = 0; j < 3; j++) {
setTimeout(() => {
console.log("let j:", j); // 0, 1, 2
}, 100);
}
What this does: Creates a new j variable for each loop iteration
Expected output: 0, 1, 2 in order
Personal tip: "Use let in for loops always - it prevents 90% of scope bugs"
Step 2: Understand Block Scope vs Function Scope
The problem: Not knowing where your variables actually exist
My solution: Learn the difference with concrete examples
function testScopes() {
// Function scope with var
if (true) {
var functionScoped = "I'm available everywhere in this function";
}
console.log(functionScoped); // "I'm available everywhere..."
// Block scope with let
if (true) {
let blockScoped = "I only exist in this block";
}
console.log(blockScoped); // ReferenceError: blockScoped is not defined
}
testScopes();
What this does: Shows where variables are actually accessible Expected output: First console.log works, second throws an error
Personal tip: "Block scope makes code more predictable - your variables stay where you put them"
Real Example: Event Handlers
Here's where I see this mess up most projects:
// Problem: All buttons log the same number
const buttons = document.querySelectorAll('.btn');
for (var i = 0; i < buttons.length; i++) {
buttons[i].addEventListener('click', function() {
console.log('Button', i, 'clicked'); // Always the last number
});
}
// Solution: Each button gets its own number
const buttons2 = document.querySelectorAll('.btn');
for (let i = 0; i < buttons2.length; i++) {
buttons2[i].addEventListener('click', function() {
console.log('Button', i, 'clicked'); // Correct number every time
});
}
What this does: Creates separate variables for each event handler Expected output: Button 0 clicked, Button 1 clicked, etc.
Personal tip: "I see this exact bug in code reviews all the time - switch to let and it disappears"
Step 3: Avoid the Temporal Dead Zone
The problem: Using let variables before declaring them crashes your code
My solution: Always declare let variables before using them
Time this saves: No more "Cannot access before initialization" errors
// This crashes your code
console.log(myVar); // ReferenceError: Cannot access 'myVar' before initialization
let myVar = "Hello";
// This works fine (but don't do it)
console.log(myOldVar); // undefined (confusing but doesn't crash)
var myOldVar = "World";
// Best practice: declare first, use second
let myGoodVar = "Declared first";
console.log(myGoodVar); // "Declared first"
What this does: Shows the difference in error behavior
Expected output: Error with let, undefined with var
Personal tip: "Temporal Dead Zone errors are actually helpful - they catch typos and logic errors early"
Common Mistake I Made
// My buggy code
function processData(items) {
for (let i = 0; i < items.length; i++) {
if (items[i].special) {
// Forgot to declare 'result'
result = processSpecialItem(items[i]); // ReferenceError
break;
}
}
let result; // Too late!
return result;
}
// Fixed version
function processData(items) {
let result; // Declare at the top
for (let i = 0; i < items.length; i++) {
if (items[i].special) {
result = processSpecialItem(items[i]); // Works perfectly
break;
}
}
return result;
}
What this does: Prevents accidental global variables and catches typos Expected output: Clean error message instead of silent bugs
Personal tip: "Declare your let variables at the top of their scope - saves debugging time"
Step 4: Know When to Use 'let' vs 'const' vs 'var'
The problem: Three ways to declare variables, which one to pick?
My solution: Simple decision tree based on your needs
// Use const for values that never change
const API_URL = "https://api.example.com";
const config = { theme: "dark", version: "2.1" };
// Use let for values that will change
let userScore = 0;
let currentPage = 1;
// Use var... almost never (legacy code only)
var oldStyleVariable = "Only if you must";
My decision process:
- Will this value change? No → use
const - Will this value change? Yes → use
let - Working with old code? Maybe keep
var(but preferlet)
Personal tip: "Start with const by default. Change to let only when you need to reassign the variable"
Practical Example: Counter Component
// Good: Clear intent with const and let
function createCounter() {
const increment = 1; // Never changes
let count = 0; // Will change
const buttons = { // Object never changes
up: () => count += increment,
down: () => count -= increment,
get: () => count
};
return buttons;
}
const myCounter = createCounter(); // Never reassigned
myCounter.up();
console.log(myCounter.get()); // 1
What this does: Makes your intent crystal clear to other developers Expected output: 1
Personal tip: "Other developers (including future you) can read this code and instantly know what changes"
What You Just Learned
You can now write JavaScript variables that behave predictably and debug scope issues in seconds instead of hours.
Key Takeaways (Save These)
- Block Scope:
letcreates variables that stay in their{}blocks - no more mysterious variable sharing - Temporal Dead Zone:
letvariables must be declared before use - this catches typos and prevents bugs - Decision Tree: Start with
const, switch toletwhen you need to change values, avoidvarin new code
Your Next Steps
Pick one based on your experience:
- Beginner: Practice with for loops and if statements using
let - Intermediate: Learn about
constwith objects and arrays (they're not completely immutable!) - Advanced: Explore how
letworks with closures and module patterns
Tools I Actually Use
- ESLint: Catches
varusage and suggestslet/constautomatically - VS Code: Shows variable scope with colored highlighting
- Chrome DevTools: Great for debugging scope issues with breakpoints
Final tip: Replace var with let in your existing code gradually. Start with for loops and you'll immediately see fewer bugs.