In a non-strict JavaScript script, what happens when a function assigns to a name that is not declared anywhere in the scope chain — for example `function init() { cache = {}; }` — and why is it a bug worth catching?
answer
- writes fail differently from reads
- nothing in the chain owns the name
- the binding outlives the call
- lands on the global object
- a typo becomes a new variable
basics
~20 sThe assignment walks the scope chain, finds no binding, and in non-strict script code creates a property on the global object instead of throwing. That accidental global outlives the call and is shared by every other script on the page or process.
solid answer
~50 sAssignment resolves the target name the same way a read does — outward along the scope chain — but the failure case is asymmetric. In non-strict script code, if no environment has the binding, the engine does not throw; it creates a property on the global object, so `cache` becomes `globalThis.cache`. The variable therefore survives the call, is visible to and writable by every other script in the same realm, and keeps whatever it references alive for as long as the realm lives. The practical damage is that a typo becomes a silent new variable rather than an error, two unrelated files can collide on a common name like `data` or `i`, and state leaks between tests or between requests. Strict code and ES modules throw a `ReferenceError` for this assignment instead, which is why the mistake mostly shows up in legacy non-module scripts.
code
javascript · 12 lines// non-strict script (not a module)
function init() {
cache = {}; // no declaration anywhere in the chain
}
init();
console.log(typeof cache); // "object" — it survived the call
console.log(globalThis.cache === cache); // true — it is a global property
console.log(delete globalThis.cache); // true — implicit globals are configurable
var declared = 1;
console.log(delete globalThis.declared); // false — top-level var is notgo deeper
Recall that leaving off let/const/var in a non-strict script does not create a local — it creates a global that outlives the function — and that you should always declare your variables.
Explain the read/write asymmetry in identifier resolution and name the consequences: cross-file collisions, retained memory, state shared between tests or requests. Know that strict code and modules turn it into a ReferenceError.
Show how you would find these in a real codebase — lint rules, snapshotting the global object's own keys and diffing, the configurable-property delete test — and how you would stage a migration of legacy scripts so the fix is loud rather than silent.
Frame it as a defaults problem: the language kept an unsafe default for compatibility and fixed it by opt-in. Be ready to argue how you choose organisation-wide defaults — modules, lint gates in CI, no new non-module scripts — so that individual reviewers are not the control.
## Reading and writing are not symmetric Identifier resolution walks the scope chain outward from the current environment record until some record has the name. If nothing does, the reference is unresolvable — and what happens next depends on the operation: - **Read** an unresolvable reference: always throws `ReferenceError`. - **Write** to an unresolvable reference: in non-strict script code, the engine creates a property on the global object and assigns to that. No error, no warning. That second rule is a compatibility relic from the earliest days of the language, and it is the whole source of accidental globals. ```js // non-strict script function init() { cache = {}; // no let/const/var anywhere } init(); console.log(typeof cache); // "object" console.log(globalThis.cache); // {} — same thing ``` ## Why it looks like a local until it isn't Inside `init`, `cache` reads exactly like a local variable: it is assigned at the top of the function and used below. Nothing in the function body distinguishes it from a declared one. The difference only appears after the call returns — the binding is still there, on the global object, holding whatever the function put in it. The most common way to create one by accident is a missing declaration keyword in a loop header or a typo in an assignment target: ```js for (i = 0; i < 10; i++) { /* i is now global */ } let result = compute(); resutl = result * 2; // typo: creates a global instead of failing ``` The typo case is the painful one. With a declared variable, misspelling the *read* side throws immediately. Misspelling the *write* side in non-strict code creates a brand-new global and leaves the intended variable untouched, so the bug surfaces later and somewhere else. ## What the damage actually is **Cross-file collisions.** Every classic script on a page shares one global object. Two libraries that both forget to declare `config` are writing to the same slot, and the last one to run wins. Debugging that means noticing that a value changes without any code you can see assigning it. **Leaked lifetime.** A global property is reachable from the global object, so it — and everything it references — is never collected while the realm lives. An implicit global holding a large cache, a DOM node, or a request object turns a per-call allocation into a permanent one. **State bleeding between runs.** In test suites and in server processes, an implicit global is shared across tests or requests. Tests pass individually and fail in sequence; a value from one request appears in the next. **Loss of scope guarantees.** The point of a local is that you can reason about it from one function's source. An implicit global takes that away for the whole realm at once. ## The one observable difference from a real global A property created by an unresolvable assignment is a **configurable** property of the global object, so it can be deleted: ```js function init() { cache = {}; } init(); delete globalThis.cache; // true — implicit globals are deletable ``` A top-level `var` in a script also becomes a global-object property, but a non-configurable one, so `delete` on it returns `false` and the binding stays. That difference is a genuinely reliable way to tell an accidental global from a declared one when you are poking at a live page, and it is a nice detail to have ready if the interviewer pushes. ## Catching them Three defences, in increasing order of strength: 1. **Lint.** A rule that forbids assigning to an undeclared identifier catches this at edit time, before it ever runs. 2. **Strict code.** In strict code the unresolvable assignment throws a `ReferenceError` instead of creating anything, turning a silent leak into a stack trace at the exact line. 3. **Modules.** ES module code is always strict and has its own module scope, so the mistake fails loudly and top-level declarations never land on the global object in the first place. The reason this question still gets asked, despite modules being the default for new code, is that the failure mode is instructive: it is the clearest example in the language of a *write* silently changing the shape of the environment rather than failing, and candidates who can explain the read/write asymmetry usually understand scope resolution properly rather than by analogy. ## Auditing an existing page If you inherit a non-module codebase and suspect leaks, snapshot the global object's own keys early (`Object.keys(globalThis)`) and diff after exercising the app; new keys that no file declares are your candidates. Combine that with the `delete` test above to separate accidental globals from intentional ones.
- How can you tell at runtime whether a global-object property came from an undeclared assignment or from a top-level `var`?Try deleting it. A property created by an unresolvable assignment is configurable, so `delete globalThis.name` returns `true` and removes it. A top-level `var` in a script creates a non-configurable property, so `delete` returns `false` and the binding survives. `Object.getOwnPropertyDescriptor(globalThis, 'name')` shows the same difference in its `configurable` flag.
- Why is an accidental global a memory problem and not just a naming problem?Because the global object is a GC root that lives as long as the realm. Anything an implicit global references — a cache, a DOM subtree, a request context — stays reachable forever, so a per-call allocation becomes permanent. In a long-lived server process or a single-page app that is a steadily growing heap rather than a bounded one.
- Assignment to an undeclared name creates a global, but reading one throws. Why the asymmetry?It is historical compatibility, not design: the earliest scripts relied on undeclared assignment as a way to create variables, and the behaviour could not be removed without breaking the web. Reads never had that excuse. The language's later fix was to change it under strict code rather than in the default script mode.
saying these in an interview costs you the question
- Says the undeclared assignment creates a local variable
- Thinks the variable disappears when the function returns
- Claims assigning to an undeclared name always throws
- Believes globalThis.x = 1 is somehow safer than the implicit form
- Assumes a linter cannot catch it because it is runtime behaviour