skip to content

Before ES2015, `typeof x` was a safe way to probe a variable that might not exist. What changed once let and const arrived, and when does typeof now throw?

level: middleimportance: should knowfreq 44%

answer

  1. safe only for names bound nowhere
  2. dead-zone bindings do resolve
  3. declaring it below breaks the probe
  4. probe a property, not an identifier

basics

~20 s

typeof is still safe for a completely undeclared name — it returns the string "undefined". But if the name is declared with let, const, or class in an enclosing scope and is still in its temporal dead zone, typeof throws a ReferenceError like any other access.

solid answer

~40 s

`typeof` has one special power: applied directly to an identifier that resolves to nothing at all, it returns `"undefined"` instead of throwing, which is why it was the classic existence check. That exemption is about *unresolvable* references. A `let`, `const`, or `class` name in its temporal dead zone is not unresolvable — the binding exists, it is simply uninitialized — so `typeof` performs a normal access on it and throws `ReferenceError: Cannot access 'x' before initialization`. In other words `typeof x` is safe when `x` is nowhere, and unsafe when `x` is declared lexically below the probe in the same or an enclosing scope. If you need a probe that never throws, test a property instead — `typeof globalThis.x` or `'x' in globalThis` — since property access on a missing key is always safe.

code

javascript · 10 lines
javascript
console.log(typeof nothingDeclaresThis); // "undefined" - safe

try {
  console.log(typeof token); // throws: token is declared below, still uninitialized
} catch (e) {
  console.log(e.constructor.name); // ReferenceError
}
let token = 'abc';

console.log(typeof globalThis.token); // "undefined" - property probe never throws

go deeper

for a junior

Remember two facts: typeof on a name that was never declared gives the string "undefined" without erroring, but typeof on a let or const used before its line still throws a ReferenceError.

for a middle

Explain the distinction that produces both behaviours — the exemption covers references that resolve to nothing, while a dead-zone binding resolves fine and fails on the value fetch.

for a senior

Show the failure as it appears in review: converting a var to const, or moving a declaration into a block, can break a typeof guard written above it, and the stack trace accuses a line that looks harmless.

for a principal

Take a position on the idiom itself: identifier existence-probing is unreliable by construction, so capability checks belong on properties of an explicit object, keeping detection out of the scope-resolution rules entirely.

## The old guarantee `typeof` is the only operator in the language with a built-in escape hatch for missing names. Applied directly to an identifier, it does not force the reference to resolve; if the name is not bound anywhere in the scope chain, the whole expression evaluates to the string `"undefined"` rather than throwing. ```js console.log(typeof neverDeclaredAnywhere); // "undefined" - no error console.log(neverDeclaredAnywhere); // ReferenceError: ... is not defined ``` Before ES2015 that made `typeof` an unconditional feature-detection idiom: every binding was either absent or already initialized, so the operator could not throw. ## What ES2015 changed `let`, `const`, and `class` introduced a state that did not previously exist: a binding that is present in the scope chain but *uninitialized*. The `typeof` exemption was never written for that case — it applies to references that cannot be resolved at all. A binding in the temporal dead zone resolves perfectly well; the failure happens one step later, when the engine tries to fetch the value from an uninitialized slot. So `typeof` throws: ```js try { console.log(typeof token); // ReferenceError: Cannot access 'token' before initialization } catch (e) { console.log(e.constructor.name); } let token = 'abc'; ``` Remove the `let token = 'abc';` line entirely and the same `typeof token` prints `"undefined"`. Adding a declaration *below* the probe is what breaks it — which is a genuinely surprising direction of causation, and the reason this question gets asked. ## The rule, stated crisply `typeof identifier` throws exactly when the identifier resolves to a binding that is uninitialized. That means: - **Safe:** the name is not declared anywhere in scope. - **Safe:** the name is a `var` (already initialized to `undefined`) or a function declaration. - **Safe:** the name is a `let`/`const`/`class` whose declaration has already been evaluated. - **Throws:** the name is a `let`/`const`/`class` in the current or an enclosing scope whose declaration has not run yet. ## Why this bites in real code The practical trap is a probe written near the top of a scope for a name that someone later declares lexically further down — often during a refactor that converts a `var` to a `const`, or that moves a declaration into the same block as the check. The probe used to answer `"undefined"` harmlessly and now takes the whole scope down, and the stack points at a `typeof` line, which reads like it cannot possibly be the culprit. A second flavour is block shadowing: an inner `const` with the same name as an outer initialized binding puts the *inner* name in the dead zone for the whole block, so a `typeof` earlier in that block throws even though an outer binding of that name exists and is perfectly initialized. ```js const feature = 'on'; { // throws - the inner binding shadows the outer one from the opening brace try { console.log(typeof feature); } catch (e) { console.log(e.constructor.name); } const feature = 'off'; } ``` ## Probes that still cannot throw If you genuinely need a check that never throws, stop probing an identifier and probe a *property*, because reading a missing property yields `undefined` rather than an error: ```js if (typeof globalThis.SomeGlobal !== 'undefined') { /* ... */ } if ('SomeGlobal' in globalThis) { /* ... */ } ``` This works only for things that are actually properties of the global object; a top-level `let` or `const` creates a binding that is *not* exposed as a property, so a property probe simply reports it as absent. That is a limitation, not a bug — and in practice it is the right answer, because a lexical binding in your own module is something you should reference directly rather than sniff for. The deeper takeaway for a modern codebase: existence-probing an identifier you wrote yourself is a code smell. `typeof` guards belong on host capabilities you do not control, and even there a property probe on the relevant object is the more honest expression of the question. ## What to say in an interview Lead with the distinction — unresolvable name versus resolved-but-uninitialized binding — because that single sentence explains both the surviving exemption and the new throw. Then give the surprising direction: adding a `let` *after* the `typeof` is what makes the `typeof` fail.

  • Why does typeof still not throw for a name that was never declared at all?
    Because the operator is defined to short-circuit an unresolvable reference: instead of fetching a value it detects that the name has no binding in the scope chain and yields the string `"undefined"`. A temporal-dead-zone name is not unresolvable — the binding is right there in the Environment Record, merely uninitialized — so the exemption does not apply and the fetch throws.
  • How would you write an existence check that cannot throw under any circumstances?
    Probe a property rather than an identifier: `typeof globalThis.Thing !== 'undefined'` or `'Thing' in globalThis`. Reading a missing property returns `undefined` instead of raising, so no dead zone is involved. The caveat is that top-level `let` and `const` bindings are not global-object properties, so this only answers questions about actual properties.
  • Can adding a declaration to a file break a typeof check that already existed?
    Yes, and that is the sharpest form of the trap. A `typeof flag` near the top of a block prints `"undefined"` while nothing declares `flag`. Introduce `const flag = ...` lower in that same block and the probe now resolves to an uninitialized binding and throws — the new declaration broke a line above it.

saying these in an interview costs you the question

  • Says typeof can never throw in JavaScript
  • Thinks typeof returns "undefined" for a TDZ binding
  • Believes only direct reads throw, not typeof
  • Assumes an outer binding is used when an inner one shadows it
  • Claims top-level let and const become globalThis properties

context