Skip to main content

The language and how it changes

1. Who owns JavaScript​

JavaScript has no owner in the sense that Rust has a foundation. It has a standard and a committee, and both exist because of a specific crisis.

1.1 The crisis​

Brendan Eich wrote the language at Netscape in 1995, in about ten days, because the browser needed a scripting language before a competitor shipped one. It was called Mocha, then LiveScript, then renamed JavaScript in December 1995 under a co-branding agreement with Sun Microsystems. The name is marketing; the two languages have little in common.

In 1996 Microsoft reverse-engineered it for Internet Explorer 3 and, unable to use the trademarked name, shipped JScript. Two implementations, two names, no agreement on behaviour, and a browser war under way. In November 1996 Netscape submitted the language to Ecma International to stop the fragmentation — Ecma rather than ISO because its process was faster.

The result is ECMA-262, defining a language called ECMAScript, a name chosen because nobody held a trademark on it.

1.2 Four names used interchangeably​

NameWhat it actually is
JavaScriptThe common name. Also a trademark held by Oracle, which acquired Sun in a deal announced 2009 and closed January 2010. Deno Land has a live USPTO petition to cancel it as generic and abandoned.
ECMAScriptThe name the standard uses for the language.
ECMA-262The standard document.
TC39Ecma Technical Committee 39, which writes and maintains ECMA-262.

Ecma International was founded in 1961 as the European Computer Manufacturers Association and dropped the acronym in 1994, which is why it is written "Ecma" and not in capitals.

1.3 What the first twenty years produced​

YearEditionNotes
19971stAdopted by the Ecma General Assembly in June.
19982ndEditorial, to align with ISO/IEC 16262.
19993rdRegular expressions, try/catch, better string handling.
20084thAbandoned after disagreement over how far the language should change.
20095thStrict mode, JSON, accessors, property-attribute reflection.
20156th (ES2015)Enormous. Six years of work landing at once.
2016–AnnualSmall, predictable, dated by year rather than edition.

The annual cycle is a reaction to the two rows above it: a collapsed edition, then one so large it took six years.

1.4 A standard needs a maintainer​

A specification nobody tends drifts out of line with what engines do, which is the fragmentation ECMA-262 was created to prevent. So the useful question is not what the standard says, but who is empowered to change it. That is TC39.

  • Who. Delegates from browser vendors, runtime maintainers such as Node, large industrial users, and invited experts.
  • How often. Roughly six plenary meetings a year.
  • How decided. Consensus — any delegate can block. Slow, and unusually robust.
  • How visible. Agendas, minutes, slides and every proposal's design discussion are public on GitHub.
The habit

When you want to know whether a feature is real, you do not have to guess. You can read the argument.

1.5 How a change happens​

Changes move through numbered stages. Know these: "stage 3" appears constantly in release notes and carries real information.

StageNameEntrance criteria
0StrawpersonNone. Someone had an idea.
1ProposalA committee champion has taken it on. Problem in prose, API sketched, cross-cutting concerns identified.
2DraftFormal spec text exists. Designated reviewers assigned by the committee, not the champion.
2.7—Design approved in principle. Spec text complete; designated reviewers and editors have signed off. test262 tests are written during this stage.
3Candidatetest262 tests are merged. Implementations expected; feedback now comes from implementers and users.
4FinishedTwo compatible implementations pass the tests, and the PR against ECMA-262 has merged.

Three things follow that the table does not show.

Stage 3 is not a promise. The dotted arrows are real. Decorators and ShadowRealm both sat at stage 3 for years and are today listed at 2.7. Do not build production code on stage 3 without understanding that.

Stage 4 requires implementations, not announcements. A feature nobody can implement does not enter the language.

The process itself changes. Stage 2.7 was added at the November 2023 plenary, because too much churn was happening at stage 3 after implementers had begun work. A fixed mental model of the process will make you wrong eventually.

1.6 The annual cycle​

A stage 4 proposal merges into the specification immediately. The yearly edition is a snapshot of whatever had merged by the cutoff.

The March branch is the cutoff that matters: a proposal finishing in April belongs to the following year's edition. This is why a feature can ship in your browser a year before it appears in a numbered edition. Ratification has in practice happened in June — ES2025, the sixteenth edition, on 25 June 2025 — though the process document now describes a July target.

Do this now

Open github.com/tc39/proposals. The README table lists stage 2 and above; stage 1 and stage 0 are in separate linked files, and shipped proposals move to finished-proposals.md. Pick one, open its repository, read an issue thread.


2. Why the language cannot fix its own mistakes​

You now know who can change the language. The more revealing question is what they refuse to change.

typeof null === "object" is a bug, and has been since 1995. The original implementation represented values as a type tag plus a payload; the tag for objects was 0, and the null pointer was all zeroes, so null read as an object.

It will never be fixed. Neither will var, the == coercion table, or Array.prototype.sort converting elements to strings by default.

2.1 The constraint​

One rule governs everything TC39 does: do not break the web.

There is no deprecation mechanism. No version flag, no semantic opt-in, no migration period, no way to reach the authors. Every page ever published must keep working, and a change breaking 0.01% of pages breaks millions of pages.

DefectWhy it survives
typeof null === "object"Live code branches on it.
var and its hoistingCould not be removed, so let and const were added alongside.
The == coercion tableAny change alters comparison results in existing pages.
sort comparing as stringsChanging the default would silently reorder existing output.

2.2 The renames​

In 2018 TC39 was standardising an array-flattening method named Array.prototype.flatten. Old MooTools versions had already defined that exact name with different behaviour on the same prototype, and shipping the native version broke real sites. Rather than break them, the committee renamed it to flat — which is why it sits slightly oddly next to flatMap. The same had already happened to Array.prototype.contains, which shipped as includes.

The generalisation

JavaScript grows almost exclusively by addition. When something in the language looks clearly wrong, the useful question is not "why hasn't this been fixed" but "what would break if it were".

2.3 Annex B, the graveyard​

ECMA-262 has an appendix titled Additional ECMAScript Features for Web Browsers: normative for browsers, normative-optional elsewhere. Its job is to specify legacy behaviour precisely enough to be interoperable, without endorsing it. It holds escape and unescape, String.prototype.substr, the string HTML wrappers such as String.prototype.blink, __proto__ as an accessor, and HTML-like comments.

Two points commonly misstated:

  1. HTML-like comments are excluded from modules, not from strict mode. A leading <!-- opens a line comment in a strict-mode script; the restriction is on source parsed with the goal symbol Module. Earlier editions did phrase it as a strict-mode rule, which is why the wrong version persists.
  2. Annex B is no longer purely an appendix. Its introduction now says some of its features are specified in the annex and some in the main body, where affected steps are marked "Normative Optional" and guarded on host support. Searching the annex alone is not sufficient.

2.4 What "do not break the web" costs​

The last section of Annex B defines an internal slot, [[IsHTMLDDA]], with sub-clauses patching ToBoolean, typeof and IsLooselyEqual. Its sole purpose is to let the HTML specification make one object behave as though it were undefined. That object is document.all.

typeof document.all; // "undefined"
document.all; // an HTMLAllCollection, which definitely exists
Boolean(document.all); // false
document.all == null; // true
document.all === null; // false

Early-2000s sites used if (document.all) to detect Internet Explorer. When other browsers implemented document.all for compatibility, those sites took the IE branch and broke. The only fix that worked was to make the object falsy, and the only legitimate way to do that was to amend the language specification.

One compatibility hack from 2003 is now a numbered step in the equality operator, permanently.

2.5 Checking whether a feature is usable​

The skill to leave this section with is knowing where to look.

SourceUse it for
tc39/proposalsCurrent stage of everything
The proposal's own repositoryDesign rationale, rejected alternatives, open problems
caniuse.comBrowser support in practice
MDN compatibility tablesPer-API support, usually more precise on details
test262Exactly what behaviour is required; often clearer than the prose

Not a listicle from 2021. A significant share of those describe stage 1 proposals as though they had shipped.


3. Where the language ends​

A question that sounds trivial and is not: is setTimeout part of JavaScript?

Open ECMA-262 and search. It is not there. Neither is console.log, fetch, localStorage, document, window, alert, XMLHttpRequest, or require.

That is not an oversight. ECMA-262 defines a language — syntax, semantics, and a deliberately small standard library — and stops. Everything else is supplied by whatever is running the language. That is the host environment, and its APIs belong to other organisations with their own specifications and release processes.

3.1 The standard library, in full​

Object, Array, Function, String, Number, Boolean, Symbol, BigInt, Math, JSON, Date, RegExp, the error types, Map, Set, WeakMap, WeakSet, WeakRef, FinalizationRegistry, Promise, Proxy, Reflect, ArrayBuffer and the typed arrays, globalThis, and a few global functions such as parseInt.

That is the whole list — and it moves: ES2026 added Temporal and DisposableStack.

3.2 Who specifies the rest​

APISpecified by
setTimeout, queueMicrotask, structuredClone, localStorage, WorkerWHATWG HTML Standard
document, Element, Node, EventTarget, AbortController, eventsWHATWG DOM Standard
fetch, Request, Response, HeadersWHATWG Fetch Standard
consoleWHATWG Console Standard
URL, URLSearchParamsWHATWG URL Standard
TextEncoder, TextDecoderWHATWG Encoding Standard
require, process, Buffer, fsNode.js

So console is standardised, just not by TC39. And setTimeout lives in a browser specification, which is why Node wrote its own version returning a Timeout object rather than a number — a difference that will bite you in week 10.

3.3 Why this matters for your project​

Your library will be consumed from a browser, from a Node server, and from a browser extension. Assume window and it breaks on the server; assume process and it breaks in the browser; assume setTimeout returns a number and it breaks in Node. Writing code that does not assume its host starts with knowing where the boundary is.

host.js
const names = [
"globalThis",
"setTimeout",
"console",
"fetch",
"structuredClone",
"URL",
"TextEncoder",
"localStorage",
"document",
"window",
"process",
"Buffer",
"require",
"queueMicrotask",
"AbortController",
];

for (const name of names) {
const exists = typeof globalThis[name] !== "undefined";
console.log(`${exists ? "yes" : " no"} ${name}`);
}

Run it with node host.js, then paste it into a browser console. For every disagreement, find the specification that defines it.


4. Reading the specification​

This section will feel the most unfamiliar and is the most valuable thing in the week.

You have been told that [] == ![] is true and that this proves JavaScript is broken. That framing is useless. The result is the output of an algorithm written down in numbered steps in a document you can open right now. By the end of this section you should be able to derive it yourself.

The specification is at tc39.es/ecma262. That URL serves the living draft — the last ratified snapshot plus everything at stage 4 since — which is why it carries next year's edition number. Dated snapshots are on the Ecma site.

4.1 Notation​

NotationMeaning
ToPrimitive(input, preferredType)An abstract operation: an internal function the specification uses to describe behaviour. Not reachable from your code. Clicking the name opens its definition.
[[Prototype]], [[Get]], [[Set]], [[Environment]]Internal slots and methods: state and behaviour objects have but you cannot address directly. Object.getPrototypeOf(x) is the public read of x.[[Prototype]].
? Foo()If this returns an abrupt completion (a throw), propagate it immediately.
! Foo()This cannot throw; the specification asserts it.
{ [[Key]]: ..., [[Value]]: ... }A Record. A struct.

Once you know those, spec text stops looking like line noise.

Abstract operations are abstract

"Abstract" is not decoration. Clause 5.2 states that the algorithms specify the required semantics and are not intended to imply any specific implementation technique, and that more efficient algorithms may exist in practice. No engine is obliged to contain a function called ToPrimitive; in optimised code these operations usually vanish into inlined fast paths that resemble nothing in the numbered steps.

What an engine may not change is what you can observe. Where a step is observable — invoking valueOf or toString, triggering a getter, hitting a Proxy trap — the engine must preserve which ones run, in what order, and how many times. The freedom is over mechanism, not over side effects.

4.2 Worked example — [] == ![]​

The operative algorithm is IsLooselyEqual(x, y), clause 7.2, Testing and Comparison Operations. Older editions called it Abstract Equality Comparison.

![] is evaluated before the comparison; it is not part of it.

  1. [] is an object. ToBoolean of any object is true. So ![] is false.
  2. The comparison is now [] == false.
  3. Types differ (Object, Boolean), so the same-type shortcut does not apply.
  4. Neither operand is null or undefined, so those rules do not apply.
  5. Step: if y is a Boolean, return IsLooselyEqual(x, ToNumber(y)). ToNumber(false) is +0.
  6. Now [] == 0. Recurse.
  7. Step: if x is an Object and y is a String, Number, BigInt or Symbol, return IsLooselyEqual(ToPrimitive(x), y).
  8. ToPrimitive([]) is called without preferredType, so the hint starts as "default" — note that a Symbol.toPrimitive method would receive "default", not "number". The algorithm then converts "default" to "number" and calls OrdinaryToPrimitive, which tries valueOf() first. Array.prototype does not override it, so Object.prototype.valueOf returns the array itself. Still an object, so rejected.
  9. Fall through to toString(). Array.prototype.toString calls join; joining an empty array gives "".
  10. Now "" == 0. Recurse.
  11. Step: x is a String, y a Number, so return IsLooselyEqual(ToNumber(x), y). ToNumber("") is +0.
  12. 0 == 0. Same type, delegates to IsStrictlyEqual, which returns true.

Each step was a lookup. The result is strange because the algorithm is long, not because it is arbitrary.

4.3 Worked example​

null >= 0; // true
null > 0; // false
null == 0; // false

No intuition produces this. If null is not equal to zero and not greater than zero, how is it greater than or equal to zero?

Because == and >= are different algorithms with different rules, and no mental shortcut covers both.

>= is defined via IsLessThan: let r be IsLessThan(x, y, true); if r is true or undefined, return false; otherwise return true. So >= is the negation of <, with the undefined case handling NaN. IsLessThan(null, 0) applies ToPrimitive to both operands, then — since neither is a String — ToNumeric. ToNumber(null) is +0, so the comparison is 0 < 0, which is false. Negated: null >= 0 is true.

> is the same machinery with operands swapped. IsLessThan(0, null) is again 0 < 0, false, returned directly. So null > 0 is false. Consistent.

== diverges. IsLooselyEqual has an early rule near the top:

If x is null and y is undefined, return true. If x is undefined and y is null, return true.

After that it never mentions null again. No step converts null to a number. The comparison falls through every subsequent rule to the final step: return false.

So null and undefined are loosely equal to each other and, with exactly one exception, to nothing else. The exception is from §2.4: IsLooselyEqual contains a step returning true when x has an [[IsHTMLDDA]] slot and y is null or undefined. That is document.all == null.

Common misconception

People say == "converts both sides to the same type". It does not do that in one step, and for null it does not do it at all. It is an ordered table of rules with recursion, and the null/undefined rule sits above the conversion rules.

The lesson is not that JavaScript is weird. It is that "JavaScript coerces types" is too coarse a statement to be useful. Different operators coerce differently, and the only way to know is to read the algorithm.

5. How engines actually run this, in short​

The previous section said no engine executes the specification literally. This is what they do instead.

Engines run a tiered pipeline, trading compilation time against execution speed and promoting a function as it proves worth optimising.

Those are V8's names. SpiderMonkey and JavaScriptCore have different names for the same shape.

5.1 Shapes and inline caches​

Objects with the same properties added in the same order share an internal structure. V8 calls it a map internally (hidden class in its older writing); SpiderMonkey a shape; JavaScriptCore a structure. The payoff is that property access compiles to "read the slot at offset 3" instead of a hash lookup.

const a = { x: 1, y: 2 };
const b = { x: 3, y: 4 }; // same shape as a
const c = { y: 4, x: 3 }; // different shape — order matters
const d = { x: 1 };
d.y = 2; // transitioned through two shapes to get here

An inline cache at a property access site remembers the shapes seen there. One shape (monomorphic) is fast, a few (polymorphic) slower, many (megamorphic) falls back to generic lookup.

5.2 Two consequences​

Performance advice ages badly. Claims that arrays beat objects, that delete must be avoided, or that try/catch blocks optimisation typically described one engine at one version and were fixed years ago. Ask of any such claim: which engine, which version, measured how?

Measurement is harder than it looks.

console.time("t");
for (let i = 0; i < 1e9; i++) {}
console.timeEnd("t");

This may finish suspiciously quickly: the loop has no observable effect and the compiler may remove it. A function timed over its first ten calls is being measured in the interpreter, before any optimising tier has looked at it.

info

A number produced by a stopwatch is not automatically a fact about your code.

Reading​

Required

Recommended

Reference for the whole course


You should now be able to​

  • State what ECMA-262 defines and what it leaves to the host environment, and identify which specification owns a given global.
  • Read the current stage of a TC39 proposal and say what that stage does and does not guarantee, including that stage 3 can regress.
  • Explain why known defects are not fixed, and predict the kind of change the committee will refuse.
  • Derive a coercion result (with practice) from the specification text, citing numbered steps.
  • Explain why an engine need not implement an abstract operation literally, and what it is nevertheless obliged to preserve.
  • Name the stages of a modern engine and explain why a timing number is not automatically a fact about your code.