Temporal Dead Zone and Hoisting: JavaScript Gotchas That Bite in Production
Some JavaScript behaviors seem counterintuitive until you understand the mechanics. These are the ones that cause real bugs.
Hoisting: What Actually Happens
JavaScript moves declarations to the top of their scope during compilation — but not initializations:
console.log(x); // undefined (not ReferenceError!)
var x = 5;
console.log(x); // 5
// What JS actually does:
var x; // declaration hoisted
console.log(x); // undefined
x = 5; // initialization stays
console.log(x); // 5
Function declarations are fully hoisted (both declaration and body):
greet(); // 'Hello!' — works before declaration
function greet() { console.log('Hello!'); }
The Temporal Dead Zone (TDZ)
let and const ARE hoisted — but accessing them before initialization throws:
console.log(x); // ReferenceError: Cannot access 'x' before initialization
let x = 5;
// The TDZ is the time between scope entry and the let/const declaration
This is actually a safety feature — it prevents the confusing undefined behavior of var.
Closure Gotcha: Loop Variables
// Classic bug with var in loops
const fns = [];
for (var i = 0; i < 3; i++) {
fns.push(() => console.log(i));
}
fns.forEach(fn => fn()); // 3, 3, 3 — not 0, 1, 2!
// Fix with let (block-scoped, new binding per iteration)
for (let i = 0; i < 3; i++) {
fns.push(() => console.log(i));
}
fns.forEach(fn => fn()); // 0, 1, 2 ✓
this Binding Surprises
class Timer {
count = 0;
start() {
// Bad: this is undefined in strict mode callbacks
setInterval(function() {
this.count++; // TypeError: Cannot set property of undefined
}, 1000);
// Good: arrow function captures this from outer scope
setInterval(() => {
this.count++; // Works correctly
}, 1000);
}
}
Floating Point
0.1 + 0.2 === 0.3 // false!
0.1 + 0.2 // 0.30000000000000004
// For money: always use integers (cents) or a library
// Store $9.99 as 999 cents, display as (999 / 100).toFixed(2)
Async/Await Error Swallowing
// Dangerous: errors silently disappear
async function riskyOperation() {
const result = await fetch('/api/data'); // What if this throws?
return result.json();
}
// Always handle at the call site
try {
const data = await riskyOperation();
} catch (err) {
console.error('Failed:', err);
}
Understanding these mechanics prevents entire classes of production bugs. TypeScript catches many of them at compile time — another reason the AI SaaS Starter Kit ships with strict TypeScript from day one.
Top comments (0)