JAVASCRIPT / SCOPE, CLOSURES, AND THIS
Global scope, function scope, and blocks
Choose between global, function, and block scope on purpose, and predict exactly which variables still exist after a closing brace.
What you will learn
- Predict whether a name still exists after a closing brace from var versus let/const
- Keep loop counters and if-branch temporaries inside their block with let or const
- Recognize an accidental global made by assigning without a declaration keyword
- Explain why an early let read throws while an early var read gives undefined
Understanding Global scope, function scope, and blocks
JavaScript creates a new scope in three situations: the top level of a script or module, the body of every function including its parameter list, and any pair of braces used as a statement, meaning the block after if, for, while, try, switch, or a bare { } standing on its own. The top level of a classic script is the global scope, shared by every script the page loads; the top level of a module looks the same but is private to that one file. Function bodies and blocks sit inside whatever region they are written in, so a name declared in one of them cannot be seen from outside it.
Which region a declaration lands in depends on the keyword, not on how deep the braces are. var skips past every block and registers its name with the nearest enclosing function body, or with the global scope when there is no enclosing function, and the engine creates that binding holding undefined the moment the function is entered. let and const attach to the innermost enclosing block instead, and stay unusable until execution reaches their declaration line, which is why reading one early throws rather than producing undefined.
The mental model worth keeping: braces are walls that let and const cannot cross, while var treats them as doorways and stops only at the wall of a function body. That is why an if block full of var declarations quietly shares one binding with the rest of the function, and why switching to let gives each block its own. Declaring in the smallest region that works means fewer live names, no collisions in the global scope that other files also write to, and values that cannot be read after the code owning them has finished.
var appName = 'scope-demo';
function report(count) {
if (count > 0) {
var total = count * 2;
let half = count / 2;
console.log('inside block:', total, half);
}
console.log('after block:', total);
try {
console.log(half);
} catch (err) {
console.log('half is gone:', err.name);
}
console.log('global is reachable:', appName);
}
report(1);
console.log('total outside the function:', typeof total);A variable lives in exactly one region, the script top level, a function body, or a pair of block braces, and var always chooses the function body while let and const choose the innermost block.
Worked examples
Shadowing in a bare block
A standalone block gives let its own binding while var written in the same block belongs to the script.
let mode = 'outer';
const tag = 'top';
{
let mode = 'inner';
var stamp = tag + '/block';
console.log('block sees mode:', mode);
}
console.log('script sees mode:', mode);
console.log('var escaped the block:', stamp);Example explained
Line 1The block's let mode is a second, unrelated binding that hides the outer one only between the braces.
Line 2tag is not redeclared in the block, so the block works with the script-level constant.
Line 3var stamp ignores the braces and is registered on the script, so it is still readable two lines later.
Loop counters after the loop
Shows that a var counter outlives its loop with the value that failed the test, while a let counter does not exist afterwards.
const seen = [];
for (var i = 0; i < 3; i++) {
seen.push(i);
}
console.log('collected:', seen.join(','));
console.log('i after the var loop:', i);
for (let j = 0; j < 3; j++) {
seen.push(j);
}
console.log('typeof j after the let loop:', typeof j);Example explained
Line 1var i belongs to the surrounding scope, so after the loop it still holds 3, the value that failed i < 3.
Line 2let j belongs to the loop's own scope, so on the next line the name j does not exist at all.
Line 3typeof is the one operator that can name a binding that does not exist without throwing, which makes it a safe way to check containment.
The global created by a missing keyword
An assignment with no declaration keyword creates a global object property in non-strict code and throws under strict mode.
function setup() {
config = { retries: 2 };
}
setup();
console.log('outside setup:', config.retries);
console.log('on globalThis:', globalThis.config.retries);
function strictSetup() {
'use strict';
try {
other = 1;
} catch (err) {
console.log('under strict mode:', err.name);
}
}
strictSetup();
console.log('typeof other:', typeof other);Example explained
Line 1config = { retries: 2 } has no let, const, or var, so non-strict code resolves the name by creating a property on the global object.
Line 2That is why the object survives after setup() returns and why globalThis.config reaches the very same object.
Line 3'use strict' inside strictSetup turns the identical pattern into a ReferenceError, so other is never created.
Line 4Run this as a plain script: inside an ES module the whole file is strict and the first assignment throws.
Important notes
In a classic script, top-level var and function declarations become properties of globalThis but top-level let, const, and class do not, so globalThis.x can be undefined while x works perfectly.
Not every pair of braces is a block: an object literal's braces are part of an expression, and a function body's braces create function scope, which is exactly why var stops there but not at an if.
Common mistakes
Writing var inside an if or for and expecting the closing brace to end it: every branch and iteration shares one function-scoped binding, so a later read can pick up a value written by code the reader believed was isolated.
Reading a let above its declaration line and expecting undefined the way var behaves: the binding exists but is uninitialized, so it throws ReferenceError: Cannot access 'x' before initialization.
Assigning without a keyword inside a function, as in count = 0, which in non-strict code creates a global that outlives the call and collides with a same-named variable in another file.
Try it yourself
Change, predict, then run
In a browser console, write a function with var status = 'start' at the top, an if (true) block that contains var status = 'done' plus let stage = 'inner', and logs of status and stage after the block. Then rewrite it so both names are declared with let or const and stay contained in the block.
Open the JavaScript workspaceCheck your understanding
A function body begins with if (flag) { var count = 1; } and then runs console.log(count). When flag is false, what happens and why?
- It throws ReferenceError, because count is only created when the block actually runs
- It logs undefined, because the var declaration belongs to the whole function body while only the assignment sits inside the block
- It logs 0, because a declared but unassigned number starts at 0
- It logs null, because the block ended without setting a value
Show answer
var declarations are processed when the function is entered, so count exists and holds undefined from the first line of the body; the block contained only the assignment, which never ran. ReferenceError is tempting, and it is exactly what you get with let count inside the block, because let attaches the binding to the braces instead of to the function body.