ECMAScript Specification Literacy
How a proposal becomes a language feature, how to read specification-style algorithm steps, and using internal slots to explain apparent language oddities precisely.
By the end of this lesson, you can
- Explain the TC39 stage process and what compatibility responsibility a shipped feature carries
- Read an abstract-operation-style algorithm and connect it to observable behavior
- Distinguish an internal slot from a regular property, and explain why it's inaccessible from user code
- Use specification concepts to explain a specific, apparent language oddity precisely, not just describe it
Why it matters
Every lesson so far has explained JavaScript’s behavior in terms of a mental model. At some point — debugging a truly obscure interoperability bug, evaluating a proposal, resolving a genuine disagreement about intended behavior — the mental model isn’t enough, and the actual normative source, the ECMAScript specification, is what settles it. This lesson is about reading that source directly, not memorizing it.
How a feature becomes JavaScript
TC39, the committee that maintains ECMAScript, advances proposals through five stages (0–4), each with a specific bar to clear:
| Stage | Meaning | Should you use it in production? |
|---|---|---|
| 0 | Strawperson — an idea, not yet a formal proposal | No |
| 1 | Proposal — the problem is well-defined, a champion is assigned | No |
| 2 | Draft — initial spec text exists, syntax is roughly settled | Experimental only, expect breaking changes |
| 3 | Candidate — spec text is complete, implementations are validating it | With caution — API is not expected to change further, but isn’t yet standard |
| 4 | Finished — shipped in the standard, implementations required | Yes |
Reading specification-style algorithms
The specification describes behavior as ordered, numbered steps operating on abstract
operations — precisely defined procedures that aren’t directly callable from JavaScript, but
describe exactly what an operation does internally. ToPrimitive, used during type coercion, is a
representative example:
ToPrimitive ( input [ , preferredType ] )
1. If input is an Object, then
a. Let exoticToPrim be ? GetMethod(input, @@toPrimitive).
b. If exoticToPrim is not undefined, then
i. Let result be ? Call(exoticToPrim, input, « hint »).
ii. If result is not an Object, return result.
iii. Throw a TypeError exception.
c. If preferredType is not present, let preferredType be "default" (roughly: try valueOf then toString).
d. ... (call valueOf, then toString, whichever the hint prefers)
2. Return input. (already a primitive)
A completion record is how the specification models “what happened” after evaluating something
— normal completion (a value), or an abrupt completion (a thrown exception, a return, a break,
a continue). The ? prefix in specification text (as in ? GetMethod(...) above) is shorthand
for “if this returns an abrupt completion, propagate it immediately” — precisely how an exception
thrown deep inside a coercion call unwinds.
Internal slots and exotic objects
An internal slot — written [[SlotName]] in the specification — is internal state attached to
an object that user code cannot read, write, or even enumerate directly; only specified abstract
operations can access it.
const arr = [];
console.log(arr.length); // 0 — a real, visible, ordinary property
// Internally, arr also has [[Prototype]], among other internal slots —
// inspectable only through specified operations like Object.getPrototypeOf,
// never as arr['[[Prototype]]'] or similar direct access.
console.log(Object.getPrototypeOf(arr) === Array.prototype); // true
An ordinary object implements the standard, default set of internal methods ([[Get]],
[[Set]], and others) exactly as specified. An exotic object — an array, a Proxy, a bound
function, and several others — overrides at least one of those internal methods with custom
behavior. An array is exotic specifically because setting an index beyond its current bounds (or
setting .length directly) triggers special internal behavior that an ordinary object’s [[Set]]
doesn’t have:
const arr = [1, 2, 3];
arr[10] = 'x';
console.log(arr.length); // 11 — [[Set]] on an exotic Array object updates length automatically
const obj = { 0: 'a', 1: 'b', 2: 'c' };
obj[10] = 'x'; // obj has no length concept at all — ordinary object behavior
Check your understanding
Or reveal the answer without checking
Answer:43 then "hello"
+ uses the 'default' hint, which prefers valueOf, so obj + 1 becomes 42 + 1 = 43. Template literal interpolation always uses the 'string' hint, which prefers toString, giving 'hello'. Both go through the same ToPrimitive algorithm with different hints.
Or reveal the answer without checking
Answer:Its specification text is complete and implementations are actively validating it, but it is not yet officially finished — use with some caution
Stage 3 means the spec text is complete and stable, with engines implementing it to find real-world issues before Stage 4 finalizes it. It's further along than an early proposal, but not yet part of the standard — usable with caution, not guaranteed unchanged.
Practice: warm-up
Using the ToPrimitive algorithm described above, predict what 1 + { valueOf: () => '5' }
evaluates to, and explain which step determines it.
Stuck? Reveal one hint at a time.
Hint 1
The object has no @@toPrimitive method, so ToPrimitive falls through to calling valueOf with the default hint.
Hint 2
valueOf returns the string "5" — a primitive, so ToPrimitive returns it as-is without falling back to toString.
Hint 3
Now + has one string operand and one number operand — recall the coercion rule from Values, Variables, and Operators for that specific combination.
Reveal the answer
Try the problem yourself before reading this. There is often more than one reasonable approach — treat this as one worked example, not the only correct answer.
ToPrimitive calls valueOf(), which returns the primitive string "5" — since it’s already a
primitive, that’s the final result, toString is never called. Now 1 + "5" runs with one number
and one string: + coerces the number to a string and concatenates, giving "15".
Practice: apply it
Explain, using specification concepts from this lesson, why Array.isArray(new Proxy([], {})) is
true even though a Proxy wraps the array rather than being one directly.
Stuck? Reveal one hint at a time.
Hint 1
Array.isArray is specified to check for the [[ProxyTarget]] internal slot too, following it through if the value is a Proxy exotic object.
Hint 2
This is different from checking a prototype chain — it’s a direct, specified unwrapping behavior unique to this one operation.
Reveal the explanation
Try the problem yourself before reading this. There is often more than one reasonable approach — treat this as one worked example, not the only correct answer.
Array.isArray is specified to recognize a Proxy exotic object by following its [[ProxyTarget]]
internal slot to the underlying target, and checking that — recursively, if the target is itself
a Proxy. This is a deliberate, specified exception, not a general rule about how other operations
treat proxies (most operations interact with a Proxy’s traps instead of unwrapping it).
Summary
- TC39 advances proposals through five stages (0–4); a shipped (Stage 4) feature carries a permanent compatibility responsibility, which is why some known oddities can never be removed.
- The specification describes behavior as abstract-operation algorithms;
?marks abrupt-completion propagation — precisely what “an exception unwinds the call stack” means formally. ToPrimitiveis the real, overridable algorithm behind everyday coercion — a customvalueOf,toString, or@@toPrimitivegenuinely controls it.- An internal slot (
[[SlotName]]) is state inaccessible to user code except through specified operations; an exotic object overrides at least one standard internal method — arrays are exotic specifically because oflength’s auto-updating behavior, nothing more general. - Explaining an apparent language oddity precisely means naming the actual specification mechanism responsible, not just restating that the behavior is surprising.