AdvancedECMAScript specification literacy~45 min

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:

TC39 stages, and the compatibility responsibility that increases at each one
StageMeaningShould you use it in production?
0Strawperson — an idea, not yet a formal proposalNo
1Proposal — the problem is well-defined, a champion is assignedNo
2Draft — initial spec text exists, syntax is roughly settledExperimental only, expect breaking changes
3Candidate — spec text is complete, implementations are validating itWith caution — API is not expected to change further, but isn’t yet standard
4Finished — shipped in the standard, implementations requiredYes

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

What does this log, and why (in terms of ToPrimitive)? const obj = { valueOf() { return 42; }, toString() { return 'hello'; }, }; console.log(obj + 1); console.log(`${obj}`);
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.

A proposal is at TC39 Stage 3. What does that tell you?
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.

  1. Hint 1

    The object has no @@toPrimitive method, so ToPrimitive falls through to calling valueOf with the default hint.

  2. Hint 2

    valueOf returns the string "5" — a primitive, so ToPrimitive returns it as-is without falling back to toString.

  3. 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.

  1. 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.

  2. 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.
  • ToPrimitive is the real, overridable algorithm behind everyday coercion — a custom valueOf, toString, or @@toPrimitive genuinely 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 of length’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.