Hoisting pitfalls and how to avoid them
The Short Answer
Hoisting is JavaScript's behavior of moving declarations to the top of their scope during compilation — but only the declarations, not the assignments. var declarations are hoisted and initialized to undefined, so accessing them before the assignment line gives undefined instead of an error. let and const are hoisted too, but they're not initialized — accessing them before declaration throws a ReferenceError (the temporal dead zone). Function declarations are fully hoisted (name + body), but function expressions and arrow functions follow variable hoisting rules.
How Hoisting Actually Works
During compilation, the JavaScript engine scans the code and registers all declarations in their respective scopes. This happens before any code executes. The key distinction is between declaration (registering the name) and initialization (assigning a value). Different declaration types get different treatment during this phase.
// What you write:
console.log(a); // undefined (not an error!)
console.log(b); // ❌ ReferenceError: Cannot access 'b' before initialization
var a = 10;
let b = 20;
// How the engine sees it (conceptually):
var a; // Declaration hoisted + initialized to undefined
// let b; // Declaration hoisted but NOT initialized (TDZ starts)
console.log(a); // undefined
console.log(b); // ReferenceError — b is in the temporal dead zone
a = 10; // Assignment stays in place
let b = 20; // TDZ ends here — b is now initialized
The var variable exists (with value undefined) from the start of its scope. The let variable exists in the scope but is in a "temporal dead zone" — the engine knows about it but refuses to let you access it until the declaration line executes.
The Temporal Dead Zone (TDZ)
The TDZ is the period between entering a scope and reaching the let/const declaration. During this window, the variable exists (it's hoisted) but any access — read or write — throws a ReferenceError. This is intentional: it catches bugs where you accidentally use a variable before it's properly set up.
function example() {
// TDZ for `name` starts here (top of block scope)
console.log(name); // ❌ ReferenceError
typeof name; // ❌ ReferenceError (even typeof!)
let name = 'Alice'; // TDZ ends — `name` is now initialized
console.log(name); // ✅ 'Alice'
}
// TDZ applies per-scope, not per-file
let x = 10;
function inner() {
// New scope — new TDZ for the inner `x`
console.log(x); // ❌ ReferenceError (not 10!)
let x = 20; // This `x` shadows the outer one
}
The second example is a common pitfall — people expect console.log(x) to read the outer x (10), but the inner let x creates a new binding in this scope. Since the inner x is hoisted (but not initialized), accessing it before the declaration hits the TDZ. The outer x is shadowed and inaccessible.
Function Hoisting
Function declarations are fully hoisted — both the name and the entire function body are available from the top of the scope. This is why you can call a function before its declaration in the source code. But function expressions (including arrow functions) assigned to variables follow the hoisting rules of that variable (var, let, or const).
// ✅ Function declarations are fully hoisted
greet('Alice'); // Works! Output: 'Hello, Alice'
function greet(name: string) {
console.log(`Hello, ${name}`);
}
// ❌ Function expressions are NOT fully hoisted
sayHi('Bob'); // TypeError: sayHi is not a function
var sayHi = function(name: string) {
console.log(`Hi, ${name}`);
};
// `var sayHi` is hoisted as undefined — calling undefined() throws TypeError
// ❌ Arrow functions assigned to const — TDZ
welcome('Carol'); // ReferenceError: Cannot access 'welcome' before initialization
const welcome = (name: string) => {
console.log(`Welcome, ${name}`);
};
Notice the different errors: var function expressions give TypeError (because sayHi is undefined, and you can't call undefined), while const/let function expressions give ReferenceError (TDZ). Function declarations just work regardless of order.
Common Pitfalls
Relying on var hoisting for logic
Using a `var` variable before its assignment and expecting `undefined` as a valid state. This makes code confusing and error-prone — readers can't tell if `undefined` is intentional or a bug.
✅Always declare variables at the top of their scope, or use `let`/`const` which enforce declaration-before-use via the TDZ.
Shadowing with let/const in the same scope
Declaring a `let` variable with the same name as an outer variable, then being surprised that the outer value isn't accessible above the declaration (TDZ of the inner variable kicks in).
✅Use distinct variable names, or be explicit about which scope you're reading from. If you need the outer value, don't shadow it.
Calling function expressions before declaration
Assuming arrow functions or function expressions assigned to variables can be called before their declaration line — they can't, because only function declarations are fully hoisted.
✅Use function declarations if you need to call before the definition, or restructure code so calls come after assignments.
Best Practices to Avoid Hoisting Bugs
Do
- ✅Use `const` by default, `let` when reassignment is needed — never `var`
- ✅Declare variables at the top of their scope before using them
- ✅Use function declarations for top-level functions that need to be called anywhere in the file
- ✅Enable the `no-use-before-define` ESLint rule to catch TDZ issues at lint time
Avoid
- ❌Using `var` — it creates confusing hoisting behavior and function-scoped leaks
- ❌Relying on hoisting to call functions before they appear in source
- ❌Shadowing outer variables with inner `let`/`const` declarations
- ❌Declaring variables inside blocks when they're needed outside (use proper scope)
Why Interviewers Ask This
Hoisting questions test your understanding of JavaScript's compilation phase and scope mechanics. Interviewers want to see that you know the difference between var, let, and const hoisting behavior, can explain the temporal dead zone, understand why function declarations work differently from function expressions, and follow best practices that avoid hoisting-related bugs. It's a foundational topic that reveals how well you understand the language's execution model.
Quick Revision Cheat Sheet
var: Hoisted + initialized to undefined — accessible before declaration (bad)
let/const: Hoisted but NOT initialized — TDZ until declaration line (ReferenceError)
Function declaration: Fully hoisted — name + body available from top of scope
Function expression: Follows variable rules — var gives undefined, let/const gives TDZ
TDZ: Temporal Dead Zone — period between scope entry and declaration where access throws
Best practice: Use const/let, declare before use, enable no-use-before-define lint rule