JavaScript is both block-scoped and function-scoped depending on the declaration keyword. Strong answers compare var, let, and const side by side, show loop-capture and switch-case pitfalls, and explain how TDZ fits into block scope.
Use this JavaScript interview question to rehearse a quick answer, common mistake, follow-up, and production pitfall.
Is JavaScript block-scoped or function-scoped?Frontend interview answer
This JavaScript interview question tests whether you can explain JavaScript scope: var vs let vs const, TDZ, and loop bug pitfalls, connect it to production trade-offs, and handle common follow-up questions.
- JavaScript scope: var vs let vs const, TDZ, and loop bug pitfalls explanation without falling back to memorized docs wording
- Scope and Block Scope reasoning, edge cases, and production failure modes
- How you would answer the most likely JavaScript interview follow-up
Short answer first
JavaScript is both, depending on what you declare with. That is the short answer. The real interview follow-up is which keyword created the binding, when var leaks across blocks, and why let/const can still throw before the declaration line because of the TDZ.
So the right interview answer is not one word. It is: 'Both, depending on declaration keyword.'
Keyword | Scope type | Redeclare in same scope? | Access before declaration | Typical bug |
|---|---|---|---|---|
| Function scope | Yes | Allowed (value is | Leaking outside blocks and loop-closure issues |
| Block scope | No | ReferenceError (TDZ) | Expecting it to behave like |
| Block scope | No | ReferenceError (TDZ) | Assuming object values become immutable |
if (true) {
var a = 1;
let b = 2;
const c = 3;
}
console.log(a); // 1 (var escapes block)
// console.log(b); // ReferenceError
// console.log(c); // ReferenceError
Follow-up one-liner
Say it this way: JavaScript is function-scoped for var and block-scoped for let/const. TDZ is part of that block scope: the binding exists for the whole block, but you cannot use it until initialization runs.
Loop behavior: the classic interview trap
var in loops creates one shared binding for all iterations. let creates a fresh binding per iteration. This is why asynchronous callbacks behave differently.
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log('var', i), 0);
}
// var 3
// var 3
// var 3
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log('let', j), 0);
}
// let 0
// let 1
// let 2
Function scope vs block scope in real code
A function body is also a block, but var ignores inner blocks inside that function and stays visible across the entire function body. let/const stay where they are declared.
function demo(flag) {
if (flag) {
var status = 'ok';
let temp = 42;
}
console.log(status); // 'ok' when flag=true
// console.log(temp); // ReferenceError
}
demo(true);
Switch/case pitfall
All case clauses share one switch block unless you add braces. In production this shows up when reducers, parsers, or command handlers reuse the same variable name in multiple cases. With let/const, duplicate names across cases can throw errors unless each case is wrapped in its own block.
const kind = 'a';
switch (kind) {
case 'a': {
const msg = 'A';
console.log(msg);
break;
}
case 'b': {
const msg = 'B';
console.log(msg);
break;
}
}
Global nuance: Script vs Module
In classic scripts, top-level var can attach to window. In ES modules, top-level bindings are module-scoped and do not become window properties. This matters when old script assumptions meet bundlers, test runners, or interview follow-ups.
// Classic script (non-module)
var legacy = 1;
let modern = 2;
console.log(window.legacy); // 1
console.log(window.modern); // undefined
Practical rules
- Default to
const; useletonly when reassignment is required.
- Avoid
varin modern codebases unless maintaining legacy code.
- For loops with async callbacks, prefer
let.
- In interviews, say 'JavaScript supports both block and function scope' and then explain keyword behavior.
Practical scenario
A dashboard uses var inside loops that register click handlers, causing every handler to point at the last row index in production.
Common pitfalls
- Mixing
varandletin the same function. - Ignoring TDZ behavior during refactors.
- Assuming switch cases create separate scope automatically.
Add tests for loop callbacks and branch-local variables, especially when migrating old code from
var to let/const.Think of var as office-wide access in a function floor, while let/const are room-specific badges. Same building, different permission boundaries.
JavaScript is not purely block-scoped or purely function-scoped. It is keyword-dependent: var is function-scoped; let/const are block-scoped. Most production bugs come from legacy var assumptions in loops, conditionals, and mixed old/new code.
Use this as one explanation rep, then continue with the JavaScript interview questions cluster or a guided prep path.