In the published text of ECMAScript 2026, the whole reason typeof null in JavaScript answers "object" is one line of a fourteen-step algorithm. Step 5 of §13.5.3.1, Runtime Semantics: Evaluation, loaded and string-matched on 2026-09-09, reads in full:
If val is null, return “object”.

Six words. No note, no caveat, no marking of any kind. Deleting that line and letting null fall through to a "null" return is the smallest change anyone could propose to a language specification. It has been proposed, at least twice, by people with the standing to land it. It has never happened.
The story every other article tells is that the fix would break the web, so the bug is permanent. That story is half right and it hides the better half. The most fully documented refusal on record was of a version of the fix that would have broken nothing — no deployed page, no shipped library, not one line of code anywhere on the internet. It was refused on a principle the committee valued more than the fix.
That refusal is the article, because the same shape recurs in every long-lived interface anyone maintains.
Step 5 and Step 12
Read the whole typeof algorithm and something else jumps out. It has fourteen steps. Two of them are accommodations of the past. Only one of them is labelled as such.
Step 5 is the null case. Plain normative text, indistinguishable in the document from the steps around it that handle strings and numbers.
Step 12 is different. In the specification’s own HTML source it carries the element id step-typeof-web-compat-insertion-point and the attribute normative-optional="", and it renders in the document with a Normative Optional tag pinned to it, linking back to §2 for what that grading means. The step itself says that if the host is a web browser, and the operand has an [[IsHTMLDDA]] internal slot, typeof returns "undefined" — for an object that plainly exists.

So the same algorithm contains an unmarked historical accident and a formally marked, deliberately engineered lie. The lie is the one with the name, the paperwork and the escape hatch. The accident is just part of the language.
Six algorithm steps in the ES2026 text carry ids ending web-compat-insertion-point — in ToBoolean, IsLooselyEqual, typeof, and the three declaration-instantiation operations. The specification has a recognised category for a place in an algorithm where the web is allowed to insert itself. typeof null is not in it.
A Tag That null and Objects Had to Share
The origin is an implementation detail that escaped. The authoritative account is JavaScript: The First 20 Years, the 189-page HOPL paper by Allen Wirfs-Brock and Brendan Eich (Proc. ACM Program. Lang. 4, HOPL, Article 77, June 2020). On page 77:13 it says:
“Brendan Eich’s recollection is that the value of typeof null was the result of a leaky abstraction in the original Mocha implementation. The runtime value of null was encoded with the same internal tag value used for object values and hence the implementation of the typeof operator returned “object” without needing any extra special-case logic.”
Leaky abstraction is not a throwaway phrase there — it carries a superscript marker pointing at the paper’s own glossary, which defines it as “an abstraction that unintentionally reveals details about the abstraction that should be hidden or private.” A co-author of the language is describing the operator as having leaked his value representation into the language surface.
MDN’s typeof page (last modified 22 May 2026) gives the mechanism a level lower: “In the first implementation of JavaScript, JavaScript values were represented as a type tag and a value. The type tag for objects was 0. null was represented as the NULL pointer (0x00 in most platforms). Consequently, null had 0 as type tag, hence the typeof return value “object”.”
Note what is not being claimed. Nobody decided that null is a kind of object. typeof read a tag and reported what the tag said, and the tag could not tell the difference. The ten-day prototype that produced this is a story a companion piece in this series tells properly. What matters here is that the operator was never lying — it faithfully reported a distinction the runtime did not make.

Refusal One: The Change Would Not Have Been Subtractive
The first serious attempt was ES5’s strict mode, published in December 2009. The HOPL paper explains the design constraint that governed what could go into it:
“This problem was avoided by making strict mode subtractive. It does not add new features to ECMAScript; instead it removes problematic features. Bug-free strict mode code when run on a browser that did not support strict mode should continue to work as its developer expected.”
That is a sharp, testable rule, and it is not the same rule as don’t break the web. It says: a strict-mode program has to keep working on an engine that has never heard of strict mode. Removing a permitted behaviour satisfies that. Adding a new one does not, because the old engine will not add it.

The paper then lists what failed the test:
“Other features on Douglas Crockford’s [2007d] list of mistakes and inconveniences were considered for strict mode but not included. For each feature, either TC39 could not reach consensus that the feature was undesirable or it was discovered that the change would not be subtractive.”
And, in the same passage, the specific verdict: “Also, changing the meaning of typeof null to return something other than “object” would not be subtractive.”
So the first refusal was not about compatibility with deployed pages at all. It was about whether a fix could be smuggled in under a mode whose entire safety argument depended on the mode adding nothing.
Refusal Two: “1JS killed that replacement”
The second attempt is the one worth knowing, and the archived es-discuss thread on typeof null records it message by message. Every quotation below was re-cut from the raw page source on 2026-09-09; the message timestamps are the archive’s own.
On 19 April 2011, Tom Schuster asked what was going to happen with typeof null. Andreas Gal replied within seven minutes: “I think there is pretty broad consensus that this will affect the web. To what extent is unclear.” He offered to ship the change to Firefox nightly users and measure the damage. Eleven minutes after that, Brendan Eich corrected the premise:
“Hang on. The proposal is to change typeof null == “null” only under opt-in to Harmony.”
Opt-in. Not a change to the language everyone was running — a change visible only inside code that had explicitly asked for the next edition’s semantics. No deployed page had asked for anything, so no deployed page could break. The compatibility argument that carries the entire folk explanation of this bug does not apply to the proposal that was actually on the table.

Then the ground moved. Just after midnight UTC on 1 January 2012, David Herman posted ES6 doesn’t need opt-in. The MIME-type and pragma version switches then under discussion, he wrote, “are painful — ugly, verbose, and a permanent tax for opting to use the new semantics.” New syntax could serve as its own implicit opt-in instead, and his summary of the payoff was one line: “And the benefit we get is just one JavaScript.”
That is the origin of “1JS” as shorthand — and it is the thing that killed the typeof null fix, because a language with no version switch has nowhere for an opt-in behaviour change to live.
On 8 May 2012, back in the typeof null thread, Herman spelled out the consequence:
“Changing typeof null always seemed questionable to me in terms of value. It doesn’t really give you significant new functionality, it just kinda seems “more sensible”. But adding it would just make things more messy, for very little gain. Since we can’t eliminate the old typeof semantics, we end up with the language having different semantics in different contexts.”
“And then once we moved to 1JS, it just seemed that much worse to have the variability, since the exact same code can have different meanings right near each other:”
His worked example — using the module syntax then under discussion — put var x = typeof null; at top level and export var y = typeof null; inside a module in the same file. Under the opt-in fix, x logs "object" and Foo.y logs "null". Two lines apart, identical expression, different answers, and nothing in the expression itself tells you which you are getting. His verdict was three words: “It’s just not worth it.”

Eich confirmed the outcome the following day, on 9 May 2012, in a message about the Object.isObject proposal the typeof null change had been offered as a substitute for:
“I think we should consider Object.isObject just because the typeof null change replaced it, but 1JS killed that replacement.”
Three accounts of the same survival, and only one of them is the one everybody repeats:
| Account | What it claims | What the archived record shows |
|---|---|---|
| The folk version | Fixing it would break deployed pages, so it can never be fixed | True of an unconditional change — Gal’s 19 April 2011 message assumes exactly that. It is not what was proposed |
| ES5 strict mode, 2009 | Considered for strict mode, left out | Rejected on subtractivity: strict mode may only remove behaviour, and this adds it (HOPL, 77:84 to 77:85) |
| Harmony opt-in, 2011 to 2012 | Would have changed zero deployed pages | Rejected anyway, under 1JS: the same source text must not mean different things in different lexical contexts |
The committee looked at a fix with no compatibility cost and declined it, because it valued one language over a better operator. That is a considerably stronger position than we couldn’t.
What the Committee Will Formalise, and What It Won’t
Set that beside the case where the same committee did the opposite.
document.all is a property of the browser Document object that must simultaneously appear not to exist and continue to work. On 15 August 2016, Anne van Kesteren opened tc39/ecma262 issue #668, titled “document.all willful violation”, with one question: “How much appetite is there to try to formalize this exception?”
The seventy-four comments that followed are the most candid thing in this hub. Mark Miller: “Of all the outstanding web reality issues, this is one of the least appetizing. But like the others, if everyone’s gonna do it and we can’t kill it, we’re better off trying to pin down the best precise semantics we can get away with.” Domenic Denicola on why it mattered: “This is more about us being concerned that we’re bad citizens by monkeypatching ToBoolean/typeof/abstract equality comparison, which is “ES’s territory”.” Miller again, on the risk: “To mix metaphors horribly, this escape hatch could easily become an avalanche.” Van Kesteren on why removal was off the table: “usage is at 5% (threshold is 0.03%) which means it’s basically impossible to ever remove.” Brian Terlson, then the specification’s editor, on the aesthetics: “The grossness of the spec seems commensurate with the grossness of the desired semantics.”
The issue was closed as completed on 18 August 2017, and the outcome is Annex B.3.6, still normatively present in ES2026:
“An [[IsHTMLDDA]] internal slot may exist on host-defined objects. Objects with an [[IsHTMLDDA]] internal slot behave like undefined in the ToBoolean and IsLooselyEqual abstract operations and when used as an operand for the typeof operator.”
Its own note is unusually blunt about scope: “There are no other known examples of this type of object and implementations should not create any with the exception of document.all.”

The HTML Standard supplies the reason, at common-dom-interfaces.html (loaded 2026-09-09): “These special behaviors are motivated by a desire for compatibility with two classes of legacy content: one that uses the presence of document.all as a way to detect legacy user agents, and one that only supports those legacy user agents and uses the document.all object without testing for its presence first.”
Read that twice. One population of pages must be told the object is absent. The other must be able to use it. The only way to serve both is an object that reports a false type, and both standards bodies wrote that down rather than pretend otherwise. (A note on a stale detail: MDN still describes this as a “willful violation” of ECMAScript. That was accurate before 2017. The two HTML Standard pages that define document.all and HTMLAllCollection contain zero occurrences of the phrase as of 2026-09-09 — both now describe the behaviour in terms of the [[IsHTMLDDA]] slot, which is precisely what issue #668 was for.)
So the specification maintains a graded vocabulary for its own regrets, defined in §2, Conformance:
| Grading in ES2026 | What it means | Instances | typeof null |
|---|---|---|---|
| Plain normative | Required of every implementation, no caveat | The other thirteen steps of typeof | Here |
| Normative Optional | Browsers must implement it; other hosts may skip it | 6 algorithm steps whose ids end web-compat-insertion-point, plus 3 whole clauses including __proto__ | No |
| Legacy | Must be implemented, and flagged as undesirable | 3 clauses: with, Object.prototype.__proto__, the legacy accessor methods | No |
| Annex B, marked (normative) | Required when the host is a browser; kept only for deployed pages | [[IsHTMLDDA]], HTML-like comments, Date.prototype.getYear and the rest | No |
Annex B states the whole doctrine in its own preamble: those features “have one or more undesirable characteristics and in the absence of legacy usage would be removed from this specification. However, the usage of these features by large numbers of existing web pages means that web browsers must continue to support them.”
typeof null qualifies for none of it. Not Legacy, not Normative Optional, not Annex B. It sits in the core language with a clean face, because a marking is a promise that something is exceptional and removable in principle, and this is neither. It is not an exception to the language. It is the language.
Sixteen Hours
For a sense of how settled that is, take the most recent attempt. On 9 August 2025 a developer filed tc39/ecma262 issue #3670. Its title, verbatim, is [bug] typeof null returns "object", expected "null" — and it opens: “Thought I’d report this, so maybe some day in the far far future, or in a parallel universe, it could be fixed.”
It was closed by a specification editor sixteen hours later, as not planned, with a one-line pointer to the contributing guide, and labelled invalid four days after that. Two comments total. Compare #668: seventy-four comments across two years, ending in new normative text.
The committee will spend two years building a bespoke internal slot so that one object can lie about its type. It will spend sixteen hours declining to discuss deleting six words. The difference is not effort or appetite. It is that document.all is an exception you can wall off, and typeof null is a fact about the language that every piece of code written since 1995 has been free to depend on.

The Constraint Was Never the Code
If you have never written a line of JavaScript, the transferable part survives the translation intact.
The technical difficulty of this fix is zero. Any competent engineer can make the change in an afternoon, in any of the major engines. The obstacle is entirely social: an unknown number of programs, written by people who cannot be contacted, running in places nobody has an inventory of, are permitted to rely on the current answer. Nobody can enumerate them, so nobody can get permission, so the answer cannot change.
Every long-lived interface accumulates decisions in that category — a field whose units were never stated, a status code returned for the wrong condition, a default that made sense once. The moment a second independent implementation exists, the cost of fixing any of them stops being a function of the code and becomes a function of how many people you would have to ask. The interesting constraint is almost never the code.
What is worth copying from TC39 is not the outcome but the bookkeeping. Faced with mistakes it could not remove, the committee built a vocabulary for them: a marking that means required, but we would delete this tomorrow if we could; a marking that means browsers must, everyone else needn’t; an internal slot scoped so narrowly that its own note names the single object allowed to have it, and forbids the creation of any other. None of that fixes anything. All of it means the next reader can tell an accident from a decision, which is the difference between a specification and a pile of behaviour.
And the refusal is worth copying too. The 2011 proposal cost nothing and was declined because the committee would not accept a language where the same expression means two things depending on where it sits. Most systems with a deprecation culture — feature flags, API versions, experimental switches — would have taken that deal without a second thought, and paid for it later in a dialect nobody can reason about locally. TC39 decided that one language was worth more than one better operator, and thirty-one years on, typeof null is still "object" because they meant it.
❓ Frequently Asked Questions
Is typeof null === "object" actually a bug, or is it intentional? Both accounts are on the record and they do not conflict. The HOPL paper attributes it to a leaky abstraction in the original implementation — null and objects shared an internal tag, so the operator had nothing to distinguish. The same paper also notes it is “arguably consistent with Java where all values are objects and null is essentially the “no object” object.” Nobody on the committee defends it as good design; the 2025 issue was closed because the behaviour is settled, not because it is correct.
What is the safe way to test for an object, then? typeof x === "object" && x !== null is the direct form and the one most codebases use. Object.getPrototypeOf and Object.prototype.toString.call(x) both give more detail if you need to separate arrays, dates and plain objects. In 2012 Eich argued in the same es-discuss thread for an Object.isObject predicate, which was never adopted; the typeof null change had been offered as its replacement, and when 1JS killed the change, the predicate went with it.
What does “1JS” actually mean? It is TC39 shorthand, used by participants rather than defined in any specification, for the commitment that there is one JavaScript rather than a versioned family of them. Its founding argument is David Herman’s 1 January 2012 es-discuss post, which proposed dropping version opt-ins entirely and letting new syntax serve as its own opt-in. The practical consequence is that the language has no mechanism by which the same expression could mean one thing here and another thing there — which is exactly what an opt-in typeof null fix would have required.
Why does typeof document.all return "undefined" when the object exists? Because two populations of old pages need contradictory answers: one detects legacy browsers by testing for document.all‘s absence, the other uses it without checking. ECMAScript resolves this with the [[IsHTMLDDA]] internal slot (Annex B.3.6), which makes an object behave like undefined in ToBoolean, IsLooselyEqual and typeof. The specification states that document.all is the only object that should ever have it.
Could a future edition still fix it? Nothing prevents it in principle, and the mechanism the committee has already built for document.all shows they will write bespoke normative machinery when the case is strong enough. But the two documented refusals were on grounds that have not changed — subtractivity and 1JS — and the most recent bug report was closed the same day it was opened. Treat it as permanent and write the x !== null guard.
Where can I check any of this myself? Everything above is in documents you can load. The algorithm is tc39.es/ecma262/2026/, §13.5.3.1 — use the dated snapshot, because the undated tc39.es/ecma262/ URL is the draft of the next edition and will not tell you what shipped. The refusals are in the es-discuss archives at esdiscuss.org and in the HOPL paper’s PDF. The document.all decision is tc39/ecma262 issue #668 on GitHub, in full, with names and dates attached.
📦 Recommended Resources
Some links below are Amazon affiliate links. If you buy through them this site earns a small commission at no extra cost to you.
Start with the free primary documents, because on this particular subject they beat every book ever written about it. JavaScript: The First 20 Years by Allen Wirfs-Brock and Brendan Eich is 189 peer-reviewed pages, free, co-authored by the man who wrote the operator — pages 77:13 and 77:84 to 77:85 carry the origin and the first refusal. The dated ECMAScript 2026 snapshot is where you check what actually shipped, as opposed to tc39.es/ecma262/, which is next year’s draft. The second refusal is in the open mailing-list archive at esdiscuss.org, and the document.all decision is tc39/ecma262 issue #668 on GitHub, all seventy-four comments of it. Read those first. The books below are for the layer around them.
- JavaScript: The Good Parts by Douglas Crockford (O’Reilly, 2008) — here for a specific reason rather than its reputation. The HOPL paper records that TC39 worked through “Douglas Crockford’s [2007d] list of mistakes and inconveniences” when deciding what ES5 strict mode could remove, and this is the book that list became. Read it as the primary document of an argument the committee actually had, and read the “Awful Parts” and “Bad Parts” appendices against the ES2026 text to see which entries got fixed, which got a Legacy marking, and which — like this one — got nothing at all. It is opinionated, occasionally wrong in hindsight, and historically load-bearing.
- Effective JavaScript: 68 Specific Ways to Harness the Power of JavaScript by David Herman (Addison-Wesley, 2012) — written by the TC39 member whose es-discuss message killed the
typeof nullfix, in the same year he sent it. That makes it an unusually direct look at how someone reasons about a language they are simultaneously helping to standardise, and the items on coercion, equality and type testing are exactly the working advice that follows from the position he took in the thread.
- Working Effectively with Legacy Code by Michael C. Feathers (Prentice Hall, 2004) — not a JavaScript book, and the reason it is here is the general argument of this article. Feathers’ subject is what changes when code has callers you cannot see and cannot ask, which is the entire difference between a fix that takes an afternoon and one that takes thirty-one years. Read it if you maintain any interface with users you have never met.
✅ Pro tip: Before you file a bug against a language, grep the specification for the behaviour first. Search the ES2026 text for the exact string and look at what marks the clause: plain normative text, a Normative Optional box, a Legacy box, or a home in Annex B. Those four gradings tell you, in seconds, whether the committee considers the thing an exception it is holding its nose over or simply part of the language — and therefore whether a bug report has anywhere to go.