cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

How to read ECMAScript

Learn to navigate the ECMAScript specification, follow abstract operations and algorithm steps, and read internal slots, methods, and error notation.

By the end, you can
  • 01
    Find a ruleFollow a JavaScript expression to the relevant syntax, operation, and built-in behaviour.
  • 02
    Read the notationTell spec-only operations from callable JavaScript, and read internal slots, methods, and ? / ! correctly.
  • 03
    Explain an outputUse small spec steps to predict observable property keys, inherited reads, and abrupt errors.

A guide to the language rules

You already know how to use JavaScript. This lesson is about finding the written rule behind something JavaScript does. ECMAScript is the language standard that states the behaviour a conforming implementation must provide. It is not a copy of a browser engine's source code.

A property read to explainPop out in the code editor (opens in a new tab)JavaScript
const cart = { tea: 3 };console.log(cart.tea);

Line 1 creates a cart whose tea property holds 3. Line 2 reads that property and prints 3. The spec tells us what this read must mean, even if two engines implement the work differently.

Definition

A specification algorithm is a series of ordered rules for required behaviour. Start from the source you can run, follow only the operations it uses, and check the observable result.

The previous lesson, JavaScript & WebAssembly, explored an engine-facing boundary. Here the focus changes: a language rule says what happens, not which machine instructions a browser chooses. You do not need to understand the whole standard before reading one useful rule.

Find your way around the spec

Start with a tiny question: why does a value change type? In the example below, the text "3" becomes a number before addition. Search for the built-in you used, then follow the linked conversion rule.

A reason to look for conversion rulesPop out in the code editor (opens in a new tab)JavaScript
const price = "3";console.log(Number(price) + 2);

Line 1 stores text, not a number. Line 2 calls Number, then adds 2, so it prints 5. We can look for abstract operations, the spec's named reusable rules, when the built-in delegates to conversion.

A small map of the specification
PartQuestionExample
SyntaxWhich source forms are allowed?A declaration such as const price = 3.
Static semanticsWhat can be decided from the source form?StringValue finds an identifier's name.
Runtime semanticsWhat happens when the code runs?Evaluation resolves a name or reads a property.
Abstract operationsWhich reusable steps do other rules call?ToPropertyKey converts a computed property name.

The spec contains clauses for language types, reusable operations, objects, expressions, statements, and built-ins. A grammar production is a written shape that source code can match. A rule headed Static Semantics can inspect that shape; Runtime Semantics describes work when it runs. Search the live ECMAScript multipage specification by heading or named operation, then follow its links. Clause numbers can move between editions.

For cart.tea, begin at Property Accessors. For a computed name such as cart[key], follow GetValue and ToPropertyKey. Stop when the few relevant steps explain your output; you do not need every clause on the page.

Read one step at a time

A spec algorithm reads like a short recipe. Let gives a name to an intermediate value; If chooses a path; Return finishes that path. Assert: records a condition the surrounding rules already guarantee. An assertion is not a message printed by your page.

A tiny observable conversionPop out in the code editor (opens in a new tab)JavaScript
const price = 3;console.log(String(price));console.log(String(undefined));

Line 1 stores the number 3. Line 2 prints the text 3. Line 3 prints the text undefined. Console display looks the same for some numbers and strings, so use typeof in your editor if you need to tell them apart.

The following is a short quotation of selected steps from ToString, not a program to paste into JavaScript. The first step returns a String unchanged; another names the output for undefined.

Spec excerpt: ToString (selected steps)Text
ToString ( arg )
1. If arg is a String, return arg.
2. If arg is a Symbol, throw a TypeError exception.
3. If arg is undefined, return "undefined".

The word arg names the input in the algorithm, not a variable from our program. The excerpt is deliberately partial: the complete operation covers numbers, objects, and other values. The callable JavaScript String(...) is not a public alias for ToString; for example, their handling of a Symbol is not identical.

Abstract operations are reusable steps

An abstract operation is a spec-defined procedure with a name and inputs. It is not an ordinary JavaScript function. For example, ToPropertyKey prepares a value to name an object property. You cannot call it as a global from a page.

A numeric key works in normal codePop out in the code editor (opens in a new tab)JavaScript
const cart = { 3: "tea" };console.log(cart[3]);console.log(cart["3"]);

Line 1 makes an ordinary object with tea under key 3. Line 2 prints tea. Line 3 also prints tea: the numeric form names the same String property as "3".

Read the exact steps below from ToPropertyKey. First it obtains a primitive with a string hint. A Symbol is already a property key; another primitive becomes a String. We will unpack the punctuation in the next sections.

Spec excerpt: ToPropertyKeyText
ToPropertyKey ( arg )
1. Let key be ? ToPrimitive(arg, STRING).
2. If key is a Symbol, return key.
3. Return ! ToString(key).

Another operation, Call, checks whether a value is callable before using its [[Call]] internal method. That is a rule about calls, not a method called Call that you add to a function.

A callable functionPop out in the code editor (opens in a new tab)JavaScript
function total(price) {  return price + 2;}console.log(total(3));

Line 1 names a function and its input. Line 2 returns price + 2. Line 4 calls it with 3 and prints 5. The spec excerpt shows the callable check and the handoff to [[Call]], not the function's addition.

Spec excerpt: Call (selected steps)Text
Call ( func, thisValue [ , argList ] )
1. If argList is not present, set argList to a new empty List.
2. If IsCallable(func) is false, throw a TypeError exception.
3. Return ? func.[[Call]](thisValue, argList).

Follow ToPropertyKey through a real value

A number is easy to convert; an object can choose how it becomes a primitive. The following computed key has a conversion hook. Symbol.toPrimitive is a JavaScript hook the spec consults while converting this object.

An object used as a property keyPop out in the code editor (opens in a new tab)JavaScript
const cart = {};const key = {  [Symbol.toPrimitive](hint) {    console.log(hint);    return 3;  },};cart[key] = "tea";console.log(cart["3"]);

Line 1 creates an empty cart. Lines 2 through 7 create an object whose hook receives a hint. Line 8 uses it as a key, printing string from line 4 before storing tea. Line 9 reads the String key "3" and prints tea. That is the two-line output.

Use the ToPropertyKey excerpt above to read the sequence: ToPrimitive asks for a string hint; the returned 3 is not a Symbol, so ToString makes the key "3". A Symbol takes the other path instead.

A Symbol remains a separate keyPop out in the code editor (opens in a new tab)JavaScript
const key = Symbol("tea");const cart = { [key]: 3 };console.log(cart[key]);console.log(cart["Symbol(tea)"]);

Line 1 creates a Symbol. Line 2 stores 3 under that Symbol. Line 3 prints 3; line 4 prints undefined because the String "Symbol(tea)" is a different key. A Symbol's description is not the key's String replacement.

Step through a computed key
Step 0 of 8Ready
Your turn: follow the blue line

Replay instrumented example code. This is not an engine debugger or a dump of internal slots.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
const key = { [Symbol.toPrimitive](hint) {  console.log(hint);  return 3;} };cart[key] = "tea";console.log(cart["3"]);
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

This replay runs the lesson's instrumented example and shows each saved step. It is not an engine debugger. Watch for the hint, the conversion result, and the final read. Back restores earlier snapshots without changing the example.

Real-life analogyA phone contact list

You look up a person by the name saved in your phone. A nickname only finds them when it leads to the saved name.

In real life: A name locates one contact
In JavaScript: A property key locates one value
In real life: The spelling matters
In JavaScript: The converted key matters
In real life: A nickname needs a lookup rule
In JavaScript: An object key needs conversion

Where the analogy stops: JavaScript also accepts unique Symbol keys; a contact list does not model that separate key type.

Now change exactly one input. The playground uses a String key, stores tea, and reads it back. It illustrates the key-to-value connection; it does not simulate the full ToPrimitive operation.

Playground: change one property name
Initial input: key is 3Pop out in the code editor (opens in a new tab)JavaScript
const key = "3";const cart = {};cart[key] = "tea";console.log(cart[key]);
Result for current input
key"3"
cart[key]tea
Try it yourself

The teaching model stores tea under "3", then reads tea from that same key.

This is a teaching model of storing and reading a String key, not an engine trace or a model of object coercion. Reset restores 3.

Internal slots describe hidden state

An internal slot is state the specification associates with an object. The notation uses double brackets. For ordinary objects, [[Prototype]] describes the link used for inherited property reads. It is not a property you can access by typing the brackets.

Read through a prototypePop out in the code editor (opens in a new tab)JavaScript
const shared = { tea: 3 };const cart = Object.create(shared);console.log(cart.tea);

Line 1 gives shared a tea value of 3. Line 2 creates cart with that object as its prototype. Line 3 prints 3 even though cart has no own tea property. The prototype link explains the result.

The selected step below comes from OrdinaryGetPrototypeOf. It describes the internal state read by the operation. An implementation may store that link differently, as long as its behaviour matches.

Spec excerpt: OrdinaryGetPrototypeOfText
OrdinaryGetPrototypeOf ( obj )
1. Return obj.[[Prototype]].
A lookalike property is not the slotPop out in the code editor (opens in a new tab)JavaScript
const shared = { tea: 3 };const cart = Object.create(shared);cart["[[Prototype]]"] = "label";console.log(Object.getPrototypeOf(cart) === shared);console.log(cart["[[Prototype]]"]);

Line 1 creates the shared object; line 2 links the cart. Line 3 sets a normal String property named "[[Prototype]]". Line 4 prints true because the real prototype is still shared. Line 5 prints label, the separate property you just made.

The public Object.getPrototypeOf can observe the prototype link. A function's ordinary .prototype property is yet another thing: it is not the [[Prototype]] slot of the function itself. Distinguishing those names prevents a very common reading mistake.

Internal methods describe what objects do

An internal method specifies an object behaviour, such as reading a property through [[Get]]. Ordinary objects use the OrdinaryGet algorithm. Other kinds of objects, including Proxy objects, can specify different internal behaviour while meeting the spec's rules.

An inherited read, then an own readPop out in the code editor (opens in a new tab)JavaScript
const shared = { tea: 3 };const cart = Object.create(shared);console.log(cart.tea);cart.tea = 5;console.log(cart.tea);

Line 1 stores 3 on shared. Line 2 gives cart that prototype. Line 3 prints 3 through inheritance. Line 4 puts an own tea value of 5 on cart. Line 5 prints 5. The two logs are 3 and 5.

These selected steps from OrdinaryGet explain the first read. It asks for the object's own property. If absent, it asks its prototype and continues there. If an own data property exists, it returns its value. This excerpt stops before the accessor case; the full algorithm also handles getters.

Spec excerpt: OrdinaryGet (selected steps)Text
OrdinaryGet ( obj, propertyKey, receiver )
1. Let propertyDesc be ? obj.[[GetOwnProperty]](propertyKey).
2. If propertyDesc is undefined, then
  a. Let parent be ? obj.[[GetPrototypeOf]]().
  b. If parent is null, return undefined.
  c. Return ? parent.[[Get]](propertyKey, receiver).
3. If IsDataDescriptor(propertyDesc) is true, return propertyDesc.[[Value]].
Step through an inherited read
Step 0 of 6Ready
Your turn: follow the blue line

Replay the instrumented property example, not the engine's internal [[Get]] implementation.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
const cart = Object.create(shared);console.log(cart.tea);cart.tea = 5;console.log(cart.tea);
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

The replay calls lesson functions for inherited and own lookup and records the actual results. Its frames illustrate the visible JavaScript example, not the hidden calls an engine chose to make. Step through the first read before the assignment.

An inherited getter uses the receiverPop out in the code editor (opens in a new tab)JavaScript
const shared = {  get label() { return this.name; },};const user = Object.create(shared);user.name = "Asha";console.log(user.label);

Lines 1 through 3 define a getter on shared. Line 4 creates user with that prototype. Line 5 sets user.name to Asha. Line 6 prints Asha because the inherited getter receives user as this, the receiver passed through the lookup.

The ? and ! shorthands

A completion is the spec's way to describe how an operation finishes: normally or abruptly, such as by throwing. A ? before a spec operation passes an abrupt completion back to its caller and otherwise takes the normal value. The next lesson will examine the full Completion Record shape.

A computed name has an observable resultPop out in the code editor (opens in a new tab)JavaScript
const key = { toString: () => "tea" };const cart = { tea: 3 };console.log(cart[key]);

Line 1 makes a key object whose toString returns tea. Line 2 stores 3 in the cart. Line 3 reads with the object key and prints 3. This is a normal completion of the key conversion.

In ToIntegerOrInfinity, the selected line Let number be ? ToNumber(arg) says not to continue to the next step if conversion throws. ReturnIfAbrupt is the name of that propagation rule, shortened by ? in spec algorithms.

Spec excerpt: ToIntegerOrInfinity (selected steps)Text
ToIntegerOrInfinity ( arg )
1. Let number be ? ToNumber(arg).
2. If number is one of NaN, +0, or -0, return 0.
A throwing conversion skips later workPop out in the code editor (opens in a new tab)JavaScript
const price = {  [Symbol.toPrimitive]() { throw new Error("bad price"); },};try {  console.log(Number(price));  console.log("after");} catch (error) {  console.log(error.message);}

Lines 1 through 3 create a value whose conversion throws bad price. Line 5 tries to convert it. Line 6 never prints after; line 8, inside the catch, prints bad price. This illustrates the same early-exit idea, without pretending we can inspect a Completion Record from JavaScript.

A ! before an operation says that this particular call is known to finish normally; it unwraps the result. In ToPropertyKey, after a primitive is not a Symbol, ! ToString(key) cannot take the Symbol error path. It does not mean logical negation, and it must not be used to ignore an error.

A normal numeric-key conversionPop out in the code editor (opens in a new tab)JavaScript
const cart = { 3: "tea" };console.log(cart[3]);

Line 1 creates tea under a numeric-looking key. Line 2 reads with 3 and prints tea. This input takes the normal conversion path. The spec's ! is a statement about its algorithm at that point, not a character you put in front of cart[3].

Syntax-directed operations follow source shapes

A syntax-directed operation applies to a piece of parsed source, called a parse node. The spec gives steps for the grammar shape that node matches. For an identifier, StringValue gives its written name; runtime evaluation is a different question: what value does that name refer to now?

One written name and one runtime valuePop out in the code editor (opens in a new tab)JavaScript
const price = 3;console.log(price);

Line 1 declares a binding written price and gives it 3. Line 2 reads the binding and prints 3. The name price came from source text; 3 is the runtime value.

The excerpt below comes from Identifiers: Static Semantics: StringValue. The line beginning Identifier : tells you which grammar form the rule applies to. The indented step gives the StringValue of the IdentifierName. It is spec notation, not a runnable declaration.

Spec excerpt: identifier StringValueText
Static Semantics: StringValue
Identifier : IdentifierName but not ReservedWord
1. Return the StringValue of IdentifierName.
Names stay separate from calculated valuesPop out in the code editor (opens in a new tab)JavaScript
const price = 3;let total = price + 2;console.log(total);

Line 1 gives price the value 3. Line 2 adds 2 and stores 5 in total. Line 3 prints 5. Both identifiers have source names; the arithmetic result is not a source name.

Real-life analogyA school attendance register

A register has a written name and a mark beside it. Reading the name is not the same as reading the mark.

In real life: A student's name is written
In JavaScript: An identifier has a source spelling
In real life: A mark is beside the name
In JavaScript: A binding has a current value
In real life: The teacher reads the mark
In JavaScript: Evaluation reads the value

Where the analogy stops: A JavaScript binding can change during execution; a register is only a comparison for name versus value.

The spec also has Runtime Semantics: Evaluation rules for grammar forms. The heading tells you whether a rule is about inspecting source or executing it. That distinction helps when a syntax error happens before the first console.log can run.

Trace a real app question

Suppose a checkout page looks up a cart field chosen by a form. You do not need an engine debugger to answer a basic question: which key did the form choose, and which object supplies that value? Make the case tiny first.

A computed field on a cartPop out in the code editor (opens in a new tab)JavaScript
const cart = { tea: 3 };const key = "tea";console.log(cart[key]);

Line 1 puts 3 under tea. Line 2 sets key to the String tea. Line 3 reads cart[key] and prints 3. Here the key needs no further conversion.

If the form supplied an object rather than a String, follow ToPropertyKey before looking for a property. Then follow the relevant [[Get]] method: own data property, inherited property, or accessor. A Proxy can show an observable property key, but its trap is app code, not a window into the engine's internals.

Log a Proxy get without guessing engine internalsPop out in the code editor (opens in a new tab)JavaScript
const cart = new Proxy({ tea: 3 }, {  get(target, key, receiver) {    console.log(String(key));    return Reflect.get(target, key, receiver);  },});console.log(cart.tea);

Line 1 wraps a cart in a Proxy. Line 2 defines a get trap; line 3 prints the key tea. Line 4 delegates the read with Reflect.get. Line 7 logs the returned 3. The two lines of output are tea and 3.

Similar-looking spec notation has different jobs
NotationKindHow to read it
ToPropertyKey(value)Abstract operationA spec instruction; not a global JavaScript function.
object.[[Get]](key, receiver)Internal methodSpec behaviour for a property read; the object can have its own implementation.
object.[[Prototype]]Internal slotSpecified object state, not a property named [[Prototype]].
? operation()Completion shorthandReturn an abrupt completion; otherwise use its value.
! operation()Completion shorthandThe spec knows this particular call completes normally.
JavaScript source or spec notation?
  • cart[3]
  • String(3)
  • ToPropertyKey(3)
  • OrdinaryGet(cart, key, receiver)
  • cart.[[Prototype]]
  • cart.[[Get]](key, receiver)
Try it yourself
0 of 6 correct

Put each card with its kind of instruction. Read the explanation for why it belongs there.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common misconceptions

The spec describes results, not the exact code or memory layout an engine must use. A replay of our example is useful for learning the steps, but it does not show actual engine frames. Keep the public output and the teaching model separate.

  • “I can call ToPropertyKey in JavaScript.” It is a spec operation, not a global function.
  • “A property named [[Prototype]] is the internal slot.” Brackets in a String name create an ordinary property.
  • “Spec ! means JavaScript not.” In algorithms it marks a known normal completion.
  • “Spec ? is a conditional expression.” In algorithms it propagates an abrupt completion.
  • “All [[Get]] methods are the same.” Different object kinds may supply different internal method behaviour.

Always read notation in context. In a JavaScript program, !price is logical negation; in an algorithm step, ! ToString(key) is a completion shorthand. Similarly, an ordinary property with brackets in its name does not become internal state merely because it looks like a spec label.

Four easily confused spec ideas
NameSpec meaningNot the same as
! before a spec callUnwrap a guaranteed normal resultThe JavaScript logical-not operator.
? before a spec callPropagate an abrupt resultThe JavaScript conditional operator.
[[Prototype]]A spec internal slotThe normal .prototype property on functions.
StringValueSyntax-directed operation on a parse nodeCalling String(...) on a runtime value.

Practice exercises

Start with printed results, then connect the result to a named rule. You can run each complete starter example in the editor and compare your prediction with the real output before reading the solution.

Exercise 1 · Warm-upPredict two key reads

The cart stores tea under a numeric-looking name. Predict the two values printed on the same console line. Explain why they match.

Starter codePop out in the code editor (opens in a new tab)JavaScript
const cart = { 3: "tea" };
console.log(cart[3], cart["3"]);

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upFind the conversion rule

    You are reviewing the key in the previous cart example. Name the abstract operation that describes how a computed key becomes a property key. Do not type a JavaScript global name that does not exist.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    const cart = { 3: "tea" };
    console.log(cart[3], cart["3"]);

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeFollow an inherited read

      Read the source in order. Which value prints before cart has an own tea property? Name the object that supplies it.

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      const shared = { tea: 3 };
      const cart = Object.create(shared);
      console.log(cart.tea);

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeSpot the skipped line

        The price object refuses conversion. Predict the console output of this complete try/catch. Explain why the line that says after is skipped.

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        try {
          Number({ [Symbol.toPrimitive]() { throw new Error("bad price"); } });
          console.log("after");
        } catch (error) {
          console.log(error.message);
        }

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeName the kind of rule

          The source spells an identifier as price and prints its value. Which kind of spec rule describes the written name? Keep it separate from the runtime operation that reads the value.

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          const price = 3;
          console.log(price);

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeApply it to a checkout page

            Your checkout page reads cart[field], but sometimes the form supplies the wrong field. Which operation tells you how the field becomes a property key? Use the starter example to confirm the normal case before debugging the form input.

            Starter codePop out in the code editor (opens in a new tab)JavaScript
            const cart = { tea: 3 };
            const field = "tea";
            console.log(cart[field]);

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              For code questions, predict the log before looking at the choices. For notation questions, ask whether the name belongs to JavaScript source, a reusable spec algorithm, or an object's specified internals.

              Reading ECMAScript quiz · 8 questionsScore: first tries count
              1. Question 1 of 8Where would you look for a reusable conversion from a value to a property key?

                Choose an answer to see the explanation.

              2. Question 2 of 8What do these two property reads print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const cart = { 3: "tea" };
                console.log(cart[3], cart["3"]);

                Choose an answer to see the explanation.

              3. Question 3 of 8Which part of [[Get]] is visible as a normal property name?

                Choose an answer to see the explanation.

              4. Question 4 of 8What does the inherited property read print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const shared = { tea: 3 };
                const cart = Object.create(shared);
                console.log(cart.tea);

                Choose an answer to see the explanation.

              5. Question 5 of 8What does ? ToNumber(arg) tell the reader?

                Choose an answer to see the explanation.

              6. Question 6 of 8What does ! ToString(key) mean in ToPropertyKey?

                Choose an answer to see the explanation.

              7. Question 7 of 8What does this computed property name print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const key = { toString: () => "tea" };
                const cart = { tea: 3 };
                console.log(cart[key]);

                Choose an answer to see the explanation.

              8. Question 8 of 8What is a syntax-directed operation applied to?

                Choose an answer to see the explanation.

              Key takeaways

              The specification is easier to read when you begin with a small observable question. Search for that source form, follow named operations, and stop once the relevant steps explain the output.

              • Syntax names source shapes; static semantics inspect them; runtime semantics explain what running them does.
              • Abstract operations such as ToPropertyKey and Call are named spec rules, not callable globals.
              • Internal slots describe state; internal methods describe object behaviour. Both use double brackets in the spec.
              • A preceding ? passes on abrupt completions; a preceding ! unwraps a result known to be normal here.
              • Syntax-directed operations select rules for a grammar production, such as StringValue for an identifier.

              Remember the one-liner.
              Read one JavaScript result, find its rule, then follow only the steps needed to explain it.

              Coming next: Spec types: Completion, Property Descriptor & more. That lesson gives a closer look at the records behind these operations, including the completions carried by ?.

              CompleteFrontend Clear concepts. Working examples.