The Proposal Pipeline: How a Behaviour Becomes Normative
A feature is not "in JavaScript" when it is designed. It is in JavaScript when two engines ship it and test262 agrees with both.
ECMA-262 is revised continuously by TC39 through a staged process whose entrance criteria are executable: a specification text, a conformance test suite, and interoperable implementations. This chapter describes the stages as gates, follows two features through them (one that changed engines before the spec, one that changed the spec because of engines), and explains what "living standard", "ES2026", and "Stage 3" each mean for code you write today.
In this chapter
Five stages, each with an executable entrance criterion
| Stage | Meaning | Entrance criteria | What a program author may assume |
|---|---|---|---|
| 0 · Strawperson | An idea a TC39 delegate is willing to present. | None. | Nothing. Most Stage 0 ideas die. |
| 1 · Proposal | The committee agrees the problem is worth solving. | A champion, a problem statement, illustrative examples, a repository. | The syntax and semantics will change. Do not build on it, even via a transpiler. |
| 2 · Draft | The committee expects the feature to be included, in some form. | Initial spec text covering the major semantics. | Shape is settled; details are not. Transpiler plugins are reasonable for experimentation. |
| 2.7 · Approved | Design is complete; awaiting tests and implementation experience. (Added 2023.) | Complete spec text, reviewed by designated reviewers and editors. | Semantics are frozen barring implementation discoveries. Test262 tests are being written. |
| 3 · Candidate | Ready for implementation. | test262 tests merged or in review; spec text complete. | Browsers ship it, often behind a flag then unflagged. This is when MDN documents it and polyfills become stable. |
| 4 · Finished | Will be in the next yearly edition. | Two independent interoperable implementations passing test262; an editor-approved pull request against ecma262. | It is JavaScript. The edition year is a citation label, not a release date. |
The gates matter more than the labels. Stage 3 does not mean "finished"; it means the design is ready to be tested against reality, and features are regularly changed or withdrawn at Stage 3 when implementers find problems. Array.prototype.group was renamed and moved to Object.groupBy at Stage 3 (chapter 14). Decorators spent years at Stage 2 through three redesigns. Object.observe reached Stage 2 in 2014 and was withdrawn in 2015 when Angular, its main motivating consumer, changed strategy. Conversely, await semantics were tightened at Stage 4, after shipping, when V8 engineers demonstrated that the specified three-tick behaviour cost measurable latency and could be reduced to one tick without observable harm (chapter 6). The process is designed so that specification and implementation correct each other, and test262 is the medium through which they do.
test262: the specification you can run
test262 is a suite of more than fifty thousand JavaScript files, each asserting one observable consequence of one specification step, with metadata naming the step (esid: sec-array.prototype.includes) and the features it requires. Every engine runs it in continuous integration and publishes results; the Test262 Report site compares engines feature by feature. A proposal cannot reach Stage 4 without tests, so writing the tests is how the specification text gets debugged: tests force the author to enumerate edge cases (what if length is a getter that throws? what if the receiver is a Proxy? what if Symbol.species is redefined?), and a step that cannot be tested is a step that is not really specified. For an engine, a test262 failure is a conformance bug by definition; for the specification, a test that two engines fail in the same way is a signal that the text is wrong or that the web depends on the deviation (chapter 14). Chapter probes in this text are informal test262 cases: each spec-layer probe asserts what conforming engines must print.
// test262 tests observable *steps*, not just results. Array.prototype.includes, per spec:
// 1. Let O be ? ToObject(this value). 2. Let len be ? LengthOfArrayLike(O). 3. ... Get(O, ToString(k)) per index.
let lengthReads = 0;
const arrayLike = { get length() { lengthReads++; return 3; }, 0: "a", 2: "c" }; // index 1 is a hole
console.log("works on any array-like:", Array.prototype.includes.call(arrayLike, "c"));
console.log("length read once:", lengthReads === 1);
console.log("holes are visited as undefined:", Array.prototype.includes.call(arrayLike, undefined) === true);
console.log("SameValueZero: NaN found, -0 equals 0:", [NaN].includes(NaN), [0].includes(-0));
// An abrupt completion from a step propagates in order: ToObject first, then length.
try { Array.prototype.includes.call(null, 1); } catch (e) { console.log("null receiver -> ToObject throws:", e.constructor.name); }
try { Array.prototype.includes.call({ get length() { throw new RangeError("len"); } }, 1); } catch (e) { console.log("getter-throwing length propagates:", e.constructor.name); }
// fromIndex is ToIntegerOrInfinity; negative counts from the end, clamped at 0.
console.log("fromIndex -Infinity clamps to 0:", [1, 2, 3].includes(1, -Infinity), "| fromIndex >= length is false:", [1].includes(1, 1));Two directions of correction
Engines changed by the specification: `Array.prototype.sort` stability
Until ES2019 the sort was permitted to be unstable, and V8 was: arrays with more than ten elements used quicksort, so equal keys could reorder. Every other engine had been stable for years. When V8 replaced its sort with TimSort (in Torque, September 2018), the committee could require stability, because no shipping engine would have to change, and the requirement became normative in ES2019. The order of events is the point: the specification followed the last implementation, and only then constrained future ones.
Specification changed by engines: `await` and PromiseResolve
ES2017's await v was specified as new Promise(resolve => resolve(v)).then(...), which for a native promise v costs a NewPromiseResolveThenableJob plus a reaction: three microtask ticks. V8 implemented it faithfully, measured the overhead in real async code, and proposed a normative change: use PromiseResolve (which returns a native promise unchanged) and PerformPromiseThen (no derived promise). Observable ordering changed for programs that interleaved await with explicit .then chains, so the committee had to decide that no deployed code depended on the old interleaving. It agreed; the change (ecma262 PR #1250) landed in 2018, V8 shipped it in 7.2, and Node 12 users saw async code get faster with no source change. The specification is a living document precisely so that this kind of correction can be made once for every engine.
// Feature detection by existence for APIs, and by parsing for syntax. Each is or was a TC39
// proposal; the label is the edition it landed in, or its stage when this text was written.
const api = {
"ES2022 class static blocks / #x in obj": () => { try { (0, eval)("class C { static #x; static { } static has(o) { return #x in o; } }"); return true; } catch { return false; } },
"ES2023 Array.prototype.toSorted": () => typeof [].toSorted === "function",
"ES2023 hashbang comments": () => { try { (0, eval)("#!/usr/bin/env node\n1"); return true; } catch { return false; } },
"ES2024 Promise.withResolvers": () => typeof Promise.withResolvers === "function",
"ES2024 ArrayBuffer.prototype.transfer": () => typeof ArrayBuffer.prototype.transfer === "function",
"ES2024 RegExp v flag": () => { try { new RegExp("[\\p{L}--[a-z]]", "v"); return true; } catch { return false; } },
"ES2025 Iterator helpers": () => typeof Iterator === "function" && typeof Iterator.prototype.map === "function",
"ES2025 Set.prototype.union": () => typeof Set.prototype.union === "function",
"ES2025 RegExp duplicate named groups": () => { try { new RegExp("(?<y>a)|(?<y>b)"); return true; } catch { return false; } },
"ES2025 Promise.try": () => typeof Promise.try === "function",
"ES2025 Float16Array / Math.f16round": () => typeof Math.f16round === "function",
"ES2025 import attributes (syntax)": () => { try { (0, eval)("(async () => import('data:text/javascript,', { with: { type: 'javascript' } }))"); return true; } catch { return false; } },
"ES2026? Error.isError": () => typeof Error.isError === "function",
"ES2026? Math.sumPrecise": () => typeof Math.sumPrecise === "function",
"ES2026? Array.fromAsync": () => typeof Array.fromAsync === "function",
"ES2026? explicit resource management (using)": () => { try { (0, eval)("{ using x = null; }"); return typeof Symbol.dispose === "symbol"; } catch { return false; } },
"Stage 3 Temporal": () => typeof Temporal === "object",
"Stage 3 decorators (syntax)": () => { try { (0, eval)("(class { @((v) => v) m() {} })"); return true; } catch { return false; } },
"Stage 3 JSON.parse source text access": () => { let seen = false; JSON.parse("1", function (k, v, ctx) { seen = ctx && "source" in ctx; return v; }); return seen; },
"Stage 3 Uint8Array.fromBase64": () => typeof Uint8Array.fromBase64 === "function",
"Stage 3 ShadowRealm": () => typeof ShadowRealm === "function",
};
for (const [label, test] of Object.entries(api)) {
let ok; try { ok = test(); } catch { ok = false; }
console.log(ok ? "✓" : "✗", label);
}
console.log("Stage labels are as of September 2026; a ✗ on an ES20xx row is an engine lagging the standard.");Feature detection by existence is itself the mechanism behind the web-compatibility problem (chapter 14): a page that detects contains today constrains what contains may mean tomorrow.
What "ES2026" means, and what it does not
Since ES2015 the specification is a living standard: the normative text is the editors' draft at tc39.es/ecma262, updated whenever a Stage 4 proposal is merged. Each June the current draft is frozen, numbered, and published by Ecma as that year's edition. An edition is therefore a snapshot for citation, not a release that engines target: browsers ship features one at a time as they reach Stage 3 or 4, months or years before the edition that will contain them, and a feature's "ES2024" label means only "merged between June 2023 and June 2024". Transpiler targets such as "target": "ES2020" in TypeScript pick a syntax and library baseline by edition, which is useful, but no browser has ever implemented exactly one edition. The practical consequence: compatibility questions are answered per feature (caniuse, MDN's browser-compat-data, Test262 Report), never per edition.
The specification is itself becoming executable
ECMA-262 is written in a controlled pseudo-code with typed values, and its tooling, ecmarkup, checks that every abstract operation call refers to a defined operation with the right arity, that every ? and ! is applied to an operation that can or cannot fail, and that algorithm steps type-check (a Completion Record is not a value; a Number is not a mathematical value). Projects such as engine262 implement the specification's algorithms one-to-one in JavaScript, and esmeta extracts a mechanised semantics from the specification text automatically and uses it to generate conformance tests and to find specification bugs by differential testing against real engines. This is where Dijkstra's dictum that testing shows the presence of bugs, not their absence, meets its practical partner: a specification that can be executed can be tested, and a specification written as an abstract machine (chapter 1) is one that can be executed. The probes in this text are a small instance of the same idea, run by hand.
Where these ideas come from
The mechanisms in this chapter, traced to the paper, standard, or system that introduced them, and how each reached JavaScript.
| Mechanism | Origin | Path into JavaScript |
|---|---|---|
| Staged standardisation with implementation gates | TC39 (Ecma Technical Committee 39), The TC39 Process document (2014) | Adopted after the ES4 failure and the monolithic ES2015 release; every feature since ES2016 has passed through Stages 0–4 individually. |
| Executable conformance suites | Ecma TC39 / test262 maintainers, test262: the ECMAScript conformance test suite (2010) | Stage 4 requires test262 coverage; engines run it in CI, so a spec change without tests cannot advance and an engine deviation is a red test rather than an argument. |
| Rolling (living) standards with yearly snapshots | WHATWG (HTML) and TC39 (ECMAScript), HTML Living Standard (2011); ECMAScript yearly editions from ES2015 (2015) | The editors' draft is the normative reference; the June edition is a snapshot for citation, which is why MDN documents features between Stage 3 and 4. |
| Formal executable specification | Shu-yu Guo, Michael Ficarra, Kevin Gibbons (editors); ecmarkup by Brian Terlson, ecmarkup and the typed algorithm steps of ECMA-262 (2016) | Algorithm steps are machine-checked for type consistency and dangling references; engines such as engine262 and the esmeta project execute the specification directly to find bugs in it. |
Primary sources
- TC39 Process Document
- TC39 proposals repository (active, finished, and inactive proposals)
- test262: ECMAScript Test Suite
- Test262 Report: per-feature engine conformance
- ecma262 PR #1250: Normative: Reduce the number of ticks in async/await
- V8 blog: Getting things sorted in V8 (stable TimSort)
- ecmarkup: the specification authoring tool
- esmeta: ECMAScript Specification Metalanguage
- engine262: an implementation of ECMA-262 in JavaScript