How to Read the ECMAScript Spec: Notation, Abstract Operations, Algorithm Steps
You don't need to read the entire ECMAScript specification — it exceeds 900 pages and nobody has it memorized. What you need is to know how to get a precise answer from the spec in 10 minutes when some JavaScript behavior confuses you. That skill will rescue you the moment [] + {} and {} + [] produce different results and you can't figure out why.
🔹 Level 1 · What You Need to Know
3 Steps to Look Up Any JS Behavior in the Spec
Step 1: Open tc39.es/ecma262 (latest) or ecma-international.org/publications-and-standards/standards/ecma-262/
Step 2: Press Ctrl+F and search for the operation name
Step 3: Follow the algorithm steps one line at a time
Lookup Example 1: How typeof Works
Search term: typeof
Spec Section 13.5.3 (The typeof Operator):
Spec pseudocode:
1. Let val be the result of evaluating UnaryExpression.
2. If val is a Reference Record, then
a. If IsUnresolvableReference(val) is true, return "undefined".
b. Set val to ? GetValue(val).
3. If val is undefined, return "undefined".
4. If val is null, return "object". ← spec basis for typeof null === 'object'
5. If val is a Boolean, return "boolean".
6. If val is a Number, return "number".
7. If val is a String, return "string".
8. If val is a Symbol, return "symbol".
9. If val is a BigInt, return "bigint".
10. val must be an Object.
a. If val has a [[Call]] internal method, return "function".
b. Return "object".
You don't need to understand every detail. Step 4 is all you need: the spec explicitly mandates that null returns "object".
Lookup Example 2: == Implicit Conversion Rules
Search term: IsLooselyEqual or Abstract Equality Comparison
Spec Section 7.2.14 gives 12 rules. Look up the one you need:
// Why is null == undefined true?
// Spec Section 7.2.14, Rule 3:
// "If x is null and y is undefined, return true."
// No conversion needed — the spec hard-codes this combination.
console.log(null == undefined); // true
console.log(null === undefined); // false
console.log(null == 0); // false (does not follow general coercion)
console.log(null == ''); // false (does not follow general coercion)
// null equals only null and undefined — hard-coded in the spec
Lookup Example 3: Array.prototype.push Behavior
Search term: Array.prototype.push
Spec Section 23.1.3.20:
Array.prototype.push ( ...items )
1. Let O be ? ToObject(this value).
2. Let len be ? LengthOfArrayLike(O).
3. Let argCount be the number of elements in items.
4. If len + argCount > 2^53 - 1, throw a TypeError exception.
5. For each element E of items, do
a. Perform ? Set(O, ! ToString(F(len)), E, true). ← index converted to string
b. Set len to len + 1.
6. Perform ? Set(O, "length", F(len), true). ← updates length property
7. Return F(len).
This explains a surprising behavior: why can push work on non-array objects?
// Any object with a length property works with push
const obj = { length: 0 };
Array.prototype.push.call(obj, 'a', 'b');
console.log(obj); // { '0': 'a', '1': 'b', length: 2 }
// The spec says push operates on ToObject(this) — it doesn't require an Array
🔸 Level 2 · How It Really Works
The Spec's Overall Structure
The ECMAScript specification (2024 edition, ECMA-262, 15th edition) is organized as follows:
ECMA-262 structure:
┌──────────────────────────────────────────────────────────────┐
│ Chapters 1-5 Introduction, conformance, terms/conventions │
│ Chapter 6 Notational Conventions ← most important │
│ Chapter 7 Abstract Operations │
│ Chapter 8 Syntax-Directed Operations │
│ Chapter 9 Executable Code and Execution Contexts │
│ Chapters 10-12 Lexical grammar, expressions, statements │
│ Chapters 13-14 Functions, classes │
│ Chapter 15 Scripts and Modules │
│ Chapter 16 Error handling and language extensions │
│ Chapters 19-28 Global object, primitives, collections, Reflect│
│ Annex A Grammar summary │
│ Annex B Web compatibility legacy features │
│ Annex C Strict mode differences │
└──────────────────────────────────────────────────────────────┘
Abstract Operations: The Spec's "Internal Functions"
Abstract Operations are functions used internally by the spec. They are not callable from JavaScript, but they describe what the engine must do.
Most common abstract operations:
┌──────────────────────────────────────────────────────────────┐
│ Operation │ Purpose │
├──────────────────────┼───────────────────────────────────────┤
│ ToPrimitive(input) │ Convert object to primitive │
│ ToNumber(argument) │ Convert to number │
│ ToString(argument) │ Convert to string │
│ ToBoolean(argument) │ Convert to boolean │
│ ToObject(argument) │ Convert to object │
│ GetValue(V) │ Extract value from a Reference │
│ PutValue(V, W) │ Assign value to a Reference │
│ IsCallable(arg) │ Check if [[Call]] internal method exists│
│ SameValueZero(x,y) │ Comparison used by includes/Map/Set │
└──────────────────────┴───────────────────────────────────────┘
ToPrimitive's full logic:
// ToPrimitive(input, hint) — hint is "number", "string", or "default"
// hint = "number" (arithmetic): try valueOf() first, then toString()
// hint = "string" (template literals): try toString() first, then valueOf()
// hint = "default" (+ operator): same as "number" (except for Date)
const obj = {
valueOf() { return 42; },
toString() { return 'hello'; }
};
console.log(obj + 1); // 43 — hint=default → valueOf() first → 42+1
console.log(`${obj}`); // 'hello' — hint=string → toString() first
console.log(obj > 10); // true — comparison hint=number → valueOf() → 42 > 10
Algorithm Step Notation
The spec uses a fixed pseudocode syntax for algorithm steps:
Spec notation reference:
┌──────────────────────────────────────────────────────────────┐
│ Notation / Phrase │ Meaning │
├─────────────────────────────┼────────────────────────────────┤
│ Let x be ... │ Declare variable x, assign │
│ Set x to ... │ Update existing variable x │
│ Return ... │ Function return │
│ Throw a TypeError │ Raise a type error │
│ Assert: ... │ Invariant (always true) │
│ If ... is true, then ... │ Conditional branch │
│ Else if ... is ... │ Else-if branch │
│ For each element E of ... │ Iteration │
│ NOTE: ... │ Informational aside │
│ ? Operation(args) │ May throw; propagate if so │
│ ! Operation(args) │ Cannot throw (assertion) │
│ [[Slot]] │ Internal slot (private field) │
│ F(n) │ Coerce integer n to Number │
└─────────────────────────────┴────────────────────────────────┘
The Precise Meaning of ? and !
? and ! are shorthands for Completion Record handling:
? Operation(args) expands to:
Let result be Operation(args).
If result is an abrupt completion, return result.
Set result to result.[[Value]].
! Operation(args) expands to:
Let result be Operation(args).
Assert: result is not an abrupt completion.
Set result to result.[[Value]].
In plain terms:
? → this operation might fail; propagate failure up the call stack
! → this operation is guaranteed to succeed (failure = spec bug)
Concrete example:
// From spec Array.prototype.push algorithm:
// Step 4: If len + argCount > 2^53 - 1, throw a TypeError exception.
// Step 5a: Perform ? Set(O, ! ToString(F(len)), E, true).
// ↑ ↑
// ?→ Set can fail !→ ToString(number) cannot fail
// Real-world error scenario:
const arr = new Array(2 ** 53 - 1); // 9007199254740991 elements
arr.push(1); // TypeError: Invalid array length
Internal Slots [[Slot]]: An Object's Private Internal Properties
Double-bracket notation denotes internal slots — internal properties that JavaScript code cannot access directly:
Common internal slots:
┌──────────────────────────────────────────────────────────────┐
│ Internal Slot │ Type │ Description │
├──────────────────────┼───────────────┼───────────────────────┤
│ [[Prototype]] │ Object/null │ Prototype chain ptr │
│ [[Extensible]] │ Boolean │ Can add new props? │
│ [[Value]] │ any │ Primitive value │
│ [[Writable]] │ Boolean │ Prop writable? │
│ [[Call]] │ Method │ Function invocation │
│ [[Construct]] │ Method │ new invocation │
│ [[BoundThis]] │ any │ bind's this │
│ [[PromiseState]] │ String │ Promise state │
│ [[PromiseResult]] │ any │ Promise result value │
└──────────────────────┴───────────────┴───────────────────────┘
// Internal slots cannot be read from JS code directly
const promise = Promise.resolve(42);
// promise.[[PromiseState]] → inaccessible
// Observe behavior instead:
promise.then(v => console.log(v)); // 42
// Reflect API exposes some internal operations (not the slots themselves)
const obj = { x: 1 };
Reflect.get(obj, 'x'); // 1 — corresponds to the [[Get]] internal method call
Spec Types vs Language Types
JavaScript has 8 language types (what developers see): Undefined, Null, Boolean, Number, BigInt, String, Symbol, Object.
The spec also defines specification types — used only inside the spec, not JavaScript values:
Specification types (not JS values):
┌──────────────────────────────────────────────────────────────┐
│ Completion Record fields: [[Type]] (normal/throw/return/ │
│ break/continue) │
│ [[Value]] (carried value) │
│ [[Target]] (label for jumps) │
│ │
│ Reference Record fields: base (base value) │
│ referencedName (prop/var name) │
│ strict (strict mode?) │
│ use: explains why foo.bar = 1 works │
│ │
│ Property Descriptor fields: [[Value]], [[Writable]], │
│ [[Get]], [[Set]], │
│ [[Enumerable]], [[Configurable]] │
│ │
│ List ordered sequence (not a JS Array) │
│ Record struct (not a JS Object) │
└──────────────────────────────────────────────────────────────┘
🔺 Level 3 · What the Spec Says
Chapter 6: Notational Conventions — Key Passages
Section 6.2 of the spec (Algorithm Conventions) states:
"The algorithms in this specification use the following conventions:
- Algorithm steps are numbered. If two steps are at the same indentation level, they are sequential.
- Steps may have sub-steps, indicated by indentation.
- A step that says 'perform' or 'let' specifies an action.
- A step that says 'if' specifies a conditional action.
- The phrase 'such that' means 'with the property that.'
- The phrase 'unless' in a step means 'if the negation of the condition is true.'"
Section 6.2.4 (The Completion Record Specification Type):
"The Completion specification type is used to explain the runtime propagation of values and control flow such as the behaviour of statements (
break,continue,return, andthrow) that perform nonlocal transfers of control.Values of the Completion type are Records with the fields defined in Table 9:
Field Name Value Meaning [[Type]] normal, break, continue, return, or throw The type of completion [[Value]] any ECMAScript language value or empty The value that was produced [[Target]] a String or empty The target label for directed control transfers
This explains why return, break, continue, and throw can "escape" the current execution flow: they all produce non-normal Completion Records. The ? shorthand in spec algorithms checks for this condition and propagates the abrupt completion upward.
Spec Chapter 12: Source Text
"12.1 Source Text ECMAScript code is expressed using Unicode code points. ECMAScript source text is a sequence of code points. All Unicode code point values from U+0000 to U+10FFFF, including surrogate code points, may occur in ECMAScript source text where permitted by the ECMAScript grammars."
This is where the spec declares that JavaScript source code is Unicode. It explains why this is legal:
// All of these variable names are valid JavaScript
const π = 3.14159;
const δ = 0.001;
const 変数 = 'hello'; // Japanese identifier
const café = true; // identifier with accent
💎 Level 4 · Edge Cases and Traps
Trap 1: What NOTE and LEGACY Mean in the Spec
Spec annotation meanings:
┌──────────────────────────────────────────────────────────────┐
│ Annotation │ Meaning │
├────────────────┼─────────────────────────────────────────────┤
│ NOTE │ Non-normative; helps understanding but │
│ │ does not alter semantics │
│ LEGACY │ Feature is in spec but discouraged │
│ OPTIONAL │ Implementation may choose not to include │
│ Annex B │ Web compat feature; non-browser envs may │
│ │ omit it │
└──────────────────┴─────────────────────────────────────────── │
Practical example: in Section 13.5.3 (typeof), you'll find:
"NOTE: The result of applying the typeof operator to an Object that implements the exotic [[Call]] internal method is the String 'function'..."
This NOTE explains why typeof class {} returns "function" (a class constructor has a [[Call]]-equivalent), but it's not a primary algorithm step — it's a clarifying aside.
Trap 2: The Spec is Not the Implementation
The spec defines observable semantics, not how to implement them:
Spec vs implementation freedoms:
┌──────────────────────────────────────────────────────────────┐
│ What the spec mandates: │
│ - Which operations must throw which errors │
│ - What operators return │
│ - Prototype chain lookup order │
│ │
│ What the spec does NOT mandate: │
│ - How to represent objects in memory │
│ (V8 uses Hidden Classes; other engines may differ) │
│ - When to garbage-collect │
│ - JIT compilation timing and strategy │
│ - Internal property storage order (only enumeration order │
│ is specified) │
└──────────────────────────────────────────────────────────────┘
// Spec mandates enumeration order (ES2015+):
// 1. Integer indices (ascending)
// 2. String keys (insertion order)
// 3. Symbol keys (insertion order)
const obj = { b: 1, a: 2, 2: 3, 1: 4 };
console.log(Object.keys(obj)); // ['1', '2', 'b', 'a']
// Integer indices sorted numerically first, then string keys in insertion order.
// But V8's internal storage layout may be completely different — that's an impl detail.
Trap 3: Deriving [] + {} vs {} + [] from the Spec
This is a classic confusing case. Here is the full derivation:
Case A: [] + {}
Expression: [] + {}
Operator: + (addition)
Spec steps (Section 13.15.3 ApplyStringOrNumericBinaryOperator):
1. Let lprim be ? ToPrimitive([], hint=default)
2. Let rprim be ? ToPrimitive({}, hint=default)
Expand step 1: ToPrimitive([], hint=default)
→ Does [] have [Symbol.toPrimitive]? No.
→ hint=default treated as hint=number
→ Try valueOf(): [].valueOf() = [] (still an object, skip)
→ Try toString(): [].toString() = '' ← empty string!
Expand step 2: ToPrimitive({}, hint=default)
→ Does {} have [Symbol.toPrimitive]? No.
→ hint=default treated as hint=number
→ Try valueOf(): ({}).valueOf() = {} (still an object, skip)
→ Try toString(): ({}).toString() = '[object Object]'
Continue main algorithm:
3. lprim = '' (String), rprim = '[object Object]' (String)
4. If Type(lprim) is String OR Type(rprim) is String:
→ both are strings; perform string concatenation
5. Return ToString(lprim) + ToString(rprim)
= '' + '[object Object]'
= '[object Object]'
console.log([] + {}); // '[object Object]'
Case B: {} + []
Expression: {} + []
But! The result depends on parsing context:
Situation 1: As a statement (typed alone on the REPL line):
{} is parsed as an empty Block Statement, NOT an object literal
+[] is the unary plus operator applied to []
→ +[] = ToNumber([]) = ToNumber('') = 0
Situation 2: As an expression (inside parentheses or on assignment RHS):
({} + []) — {} is parsed as an object literal
→ ToPrimitive({}) = '[object Object]'
→ ToPrimitive([]) = ''
→ '[object Object]' + '' = '[object Object]'
// Verify both results:
console.log({} + []); // 0 (statement: {} = empty block)
console.log(({} + [])); // '[object Object]' (forced expression context)
const x = {} + [];
console.log(x); // '[object Object]' (RHS is an expression)
Decision tree for the + operator:
a + b
│
ToPrimitive(a) and ToPrimitive(b)
│
┌──────────────┴──────────────┐
Either is a String? Neither is a String
│ │
String concatenation ToNumber(both)
ToString(a) + ToString(b) Numeric addition
Chapter Summary
-
You don't need to read the entire spec. The key skill is using
Ctrl+Fto search for operation names (e.g.,typeof,IsLooselyEqual,ToPrimitive), finding the relevant section, and following the algorithm steps. When behavior confuses you, the spec is the only trustworthy source. -
?and!are Completion Record shorthands:?means the operation might throw — errors auto-propagate;!means the operation is guaranteed to succeed (failure would be a spec bug). Once you know this, spec algorithms become dramatically more readable. -
Internal slots
[[Slot]]are an object's private internal properties, inaccessible from JavaScript code but observable indirectly throughReflect,Proxy,Object.getOwnPropertyDescriptor, and similar APIs. -
Spec types (Completion Record, Reference Record, etc.) are not JavaScript values — they are tools the spec uses to describe its own algorithms. Understanding Reference Record explains why
foo.bar = 1correctly assigns a value instead of merely readingbar. -
[] + {}and{} + []differ because of parsing context: the former concatenates twoToPrimitiveresults as strings; the latter, when parsed as a statement, treats{}as an empty block statement, making+[]a unary operation that evaluates to0.