cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

Syntax, early errors & static semantics

Read ECMAScript grammar rules, trace early errors and static semantics, and see why some JavaScript scripts fail before their first statement runs.

By the end, you can
  • 01
    Read a grammar ruleIdentify a production, its optional parts, parameters, lookahead, and restricted line breaks.
  • 02
    Locate a rejectionTell parsing and early errors from errors caused by executing an otherwise valid script.
  • 03
    Use static semanticsExplain how BoundNames, VarDeclaredNames, and parameter checks describe written code.

What syntax checks before code runs

Syntactic grammar describes how JavaScript tokens fit into expressions, statements, scripts, and modules. The previous Lexical grammar & ASI lesson made those tokens. This lesson reads the rules that combine them. An engine may use a different parser design, but it must accept and reject the same language.

One small valid programPop out in the code editor (opens in a new tab)JavaScript
const cart = { tea: 3 };console.log(cart.tea);

Line 1 creates cart with a tea value of 3. Line 2 reads the property and prints 3. Before that read can happen, the source must form a valid script and satisfy any rules checked from the source itself.

Definition

Early errors reject a script or module before its first evaluation. Static semantics are rules about parsed source, including early errors and operations that collect written names.

This follows How to read ECMAScript: instead of tracing a property read, we now ask whether the source may run at all. For engine implementation and syntax trees, see Parsing and ASTs and Lazy parsing. Here we read the language specification, not an engine's parser.

Read a grammar production

A production names a source shape. Its left side is a nonterminal, a name for a group of possible shapes. Its right side contains literal tokens or other nonterminals. A syntactic production has one colon; the lexical grammar used two.

A declaration with a valuePop out in the code editor (opens in a new tab)JavaScript
let price = 3;console.log(price);

Line 1 declares price and initializes it to 3. Line 2 prints 3. In the spec, names like BindingIdentifier and Initializer describe parts of a declaration; they are not functions to call.

Selected ECMAScript grammar shapesText
VariableStatement : var VariableDeclarationList ;
VariableDeclaration : BindingIdentifier Initializer[+In]opt
WhileStatement : while ( Expression ) Statement

Read the last line aloud: while, an opening parenthesis, an expression, a closing parenthesis, then a statement. Each is one piece of a valid while statement. The declaration line uses opt to allow an initializer to be absent. The displayed lines are selected productions, not a full grammar or runnable JavaScript.

Five grammar marks to recognize
MarkMeaningUse
Statement : ...One colon: syntactic productionA shape of tokens that can form a statement.
Identifier :: ...Two colons: lexical productionA shape of source characters that forms a token.
Initializer[+In]optParameter plus optional symbolThe initializer may be absent; if present, In is enabled.
[lookahead not in ...]Restricted next tokensThis alternative cannot start with certain tokens.
[no LineTerminator here]Restricted line breakThat production cannot cross a line terminator there.

The grammar's goal is Script or Module. The same token spelling can face different restrictions depending on that goal. Parsing produces parse nodes, a spec way to describe spans of source, not a required engine data structure. The spec's syntactic grammar explains that boundary.

Grammar parameters change which shapes are allowed

A grammatical parameter chooses a version of a rule for its context. [Yield] matters inside generators; [Await] matters in async functions and modules; [In] helps distinguish the in operator from a for header. [Return] allows return statements in function bodies, not at ordinary script top level.

Yield is an operation inside a generatorPop out in the code editor (opens in a new tab)JavaScript
function* orders() {  const order = yield "tea";  console.log(order);}const list = orders();console.log(list.next().value);list.next("cake");

Line 1 defines a generator. Line 2 yields tea and waits. Line 3 will log the value sent back later. Line 5 makes an iterator, line 6 prints tea, and line 7 resumes it with cake, which line 3 prints. The output is tea then cake.

In works in a normal expressionPop out in the code editor (opens in a new tab)JavaScript
const cart = { tea: 3 };console.log("tea" in cart);

Line 1 makes a cart with a tea key. Line 2 checks whether the key exists and prints true. The spec uses +In to permit that operator in this grammar position and ~In to remove it where it would conflict with part of a for statement. A ?In reference passes the current setting down.

The Yield and Await settings are also about identifiers, not only operators. A generator cannot bind yield as its own local name, and an async function cannot bind await. Module code also prohibits await as an identifier. The spec's identifier early errors give the exact contexts.

Await cannot be a binding herePop out in the code editor (opens in a new tab)JavaScript
console.log("before");
async function total() {
  let await = 3;
}
Yield cannot be a binding herePop out in the code editor (opens in a new tab)JavaScript
console.log("before");
function* total() {
  let yield = 3;
}
Module await identifier, for inspectionJavaScript
console.log("before");
const await = 3;

In each failed example, line 1 would log before, but no log happens: the source cannot start. The module example needs a module goal, so it is not an Edit & run script; the test checks it as an .mjs file. These restrictions depend on context, not on a global list of forbidden spellings.

Optional parts and lookahead

The suffix opt means that a grammar part may be omitted. It is not JavaScript optional chaining. A lookahead restriction tells a production which next tokens it may or may not see. The next token matters because some forms, such as an expression statement starting with {, could be read another way.

A simple expression statementPop out in the code editor (opens in a new tab)JavaScript
const order = 3;console.log(order);

Line 1 creates order with value 3. Line 2 has a call expression used as a statement and prints 3. A call can begin this kind of statement. The lookahead rule blocks some other starts so the grammar does not confuse a block or declaration with an expression statement.

A simplified reading aid for lookaheadText
ExpressionStatement : [lookahead not in { {, function, class, let [ }] Expression ;

This line is a teaching paraphrase, not an exact copy of the production. Read it as: before using this expression-statement alternative, check the next tokens against the excluded starts. The exact expression-statement syntax is in the current spec.

Real-life analogyA school attendance register

A register has a space for a mark beside each name. A blank mark does not remove the name, and the next name begins a new row.

In real life: A name starts one row
In JavaScript: A token begins one source form
In real life: A mark may be blank
In JavaScript: An opt grammar part may be absent
In real life: The next row has another name
In JavaScript: Lookahead checks which token comes next

Where the analogy stops: A parser follows formal grammar rules; a teacher can infer intent from handwriting.

An early error stops the whole script

A successful parse is not enough. The spec applies early error rules to the parsed script or module before evaluation. An invalid lexical name can therefore prevent a log before the offending declaration from running. This is about one source unit, not a claim that every other script on a page is stopped forever.

A declaration that runsPop out in the code editor (opens in a new tab)JavaScript
let price = 3;console.log(price);

Line 1 binds price to 3. Line 2 prints 3. Now keep the first log but add two let declarations of the same name in the same scope.

Duplicate let rejects the entire scriptPop out in the code editor (opens in a new tab)JavaScript
console.log("before");let price = 3;let price = 4;console.log("after");

Line 1 asks to print before, but nothing is printed. Lines 2 and 3 declare the same lexical name; the whole script is rejected before line 1 executes. Line 4 never runs either. The test writes this source to a file and runs Node, asserting a SyntaxError and empty standard output rather than trusting the console panel alone.

A runtime error happens laterPop out in the code editor (opens in a new tab)JavaScript
console.log("before");try {  missingPrice();} catch (error) {  console.log(error.name);}

Line 1 prints before. Line 3 tries to call a missing name. That lookup fails during execution. Line 4 catches the error and line 5 prints ReferenceError. Both logs appear: before, then ReferenceError. Unlike a compile-time SyntaxError, a runtime error can be handled by this try/catch.

Which phase rejects the code?
CaseWhyObservable effect
Grammar mismatchThe tokens cannot form the selected Script or Module goal.The source is rejected before that unit runs.
Early errorThe shape parses, but a static rule rejects it.The source is rejected before that unit runs.
Runtime errorValid source starts evaluating, then an operation throws.Earlier effects in the same script may already have happened.
Compare when a problem is found
The model behind the selectorPop out in the code editor (opens in a new tab)JavaScript
function describeEarlyCheck(kind) {  return kind === "duplicate"    ? "reject before running"    : "run until the call fails";}console.log(describeEarlyCheck("duplicate"));
Selected caseDuplicate let

reject before running

Try it yourself

This teaching model says: reject before running. The examples above show the actual JavaScript difference.

This selector runs the lesson's model function; it does not compile or evaluate typed code. Reset selects duplicate let.

Switch only the case selector, then Reset to return to the duplicate case. The selector uses a labeled teaching model; it never compiles user input. This distinction matters when a build reports a syntax problem even though a log before the bad line exists.

One initial shape can cover two readings

A cover grammar accepts an initial token shape that might have more than one use. Later the surrounding context requires it to match a more specific supplemental grammar. CoverParenthesizedExpressionAndArrowParameterList is the spec name for the parentheses that might group an expression or form the parameters of an arrow function.

Parentheses before an arrowPop out in the code editor (opens in a new tab)JavaScript
const add = (price) => price + 2;console.log(add(3));

Line 1 defines add; the parentheses hold the parameter price because an arrow follows. Line 2 calls it with 3 and prints 5. The arrow selects the arrow-parameter reading, not a call to two grammars.

The same punctuation groups an expressionPop out in the code editor (opens in a new tab)JavaScript
const price = (3);console.log(price);

Line 1 groups the value 3; no arrow follows. Line 2 prints 3. A parser cannot just call every parenthesized form a parameter list: context decides which supplemental rule must fit. The grouping rule requires a parenthesized expression.

A comma is not allowed at the end of a grouped expressionPop out in the code editor (opens in a new tab)JavaScript
console.log("before");
const price = (3,);
console.log(price);

Line 1 never prints. Line 2 looks close to an arrow parameter list with a trailing comma, but no arrow follows. As an expression in these parentheses, (3,) does not fit the supplemental grammar; line 3 is never reached. That is a pre-execution SyntaxError, not a value equal to 3.

Replay a parenthesis choice
Step 0 of 4Ready
Your turn: follow the blue line

Replay instrumented teaching code, not the specification's parser.

Running in
  1. script
Next: line 4
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
function chooseParenthesizedForm(hasArrow) {  return hasArrow ? "arrow parameters" : "grouped expression";}
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 the lesson's tiny choice function and records its result. It models why context matters, not how a browser parses tokens. Real cover grammars use the complete source form and apply early errors to the required reading.

Static semantics ask questions about written names

BoundNames is a syntax-directed operation that collects names introduced by a binding pattern. A syntax-directed operation follows the production a parse node matched; it does not read a current runtime variable. In a destructuring declaration, the object property name and the new local name can differ.

One key and a different bound namePop out in the code editor (opens in a new tab)JavaScript
const { tea: price } = { tea: 3 };console.log(price);

Line 1 takes the tea property, whose value is 3, and binds it under the local name price. Line 2 prints 3. For this pattern, BoundNames includes price, not tea. The operation examines source names; it does not return the runtime number.

Two var declarations can share a namePop out in the code editor (opens in a new tab)JavaScript
var price = 3;var price = 4;console.log(price);

Line 1 initializes price to 3. Line 2 updates the same var binding to 4. Line 3 prints 4. VarDeclaredNames collects names of var-scoped declarations; LexicallyDeclaredNames collects names from lexical declarations such as let and const. Collecting a name does not itself mean a duplicate is illegal; an early error rule decides which collisions to reject.

A lexical name collisionPop out in the code editor (opens in a new tab)JavaScript
console.log("before");
const { tea: price } = { tea: 3 };
let price = 4;

Line 1 still prints nothing. Line 2 binds price through the pattern, and line 3 also binds price. Static name checks find the collision before the script evaluates. A linter may give a friendlier message, but the language rule does not depend on the linter.

Named static questions in the spec
OperationWhat it inspectsThis lesson's example
BoundNamesNames bound by a declaration or patternconst {tea: price} = cart binds price, not tea.
LexicallyDeclaredNamesNames from lexical declarations in a statement listTwo let price declarations collide in one scope.
VarDeclaredNamesNames from var-scoped declarationsTwo var price declarations can coexist.
IsSimpleParameterListWhether every parameter is a plain binding identifierprice = 3 makes a list non-simple.
ContainsUseStrictWhether a function body has a use strict directiveA body directive with non-simple parameters is rejected.
Replay a duplicate-name check
Step 0 of 4Ready
Your turn: follow the blue line

Replay instrumented teaching code, not a JavaScript engine debugger.

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
function hasDuplicateName(names) {  return new Set(names).size !== names.length;}console.log(hasDuplicateName(names));
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's Set is a deliberately small teaching model of comparing written names. It is not the spec's BoundNames algorithm and is not an engine parser dump. The function and its edge cases are tested separately.

Function heads have extra early-error rules

IsSimpleParameterList asks whether all parameters are plain names. A default value, rest parameter, or destructuring pattern makes the list non-simple. ContainsUseStrict checks for a "use strict" directive in the function body. The spec combines these facts when validating function declarations.

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

Line 1 declares total with one plain parameter, price. Line 2 adds 2. Line 4 passes 3 and prints 5. This parameter list is simple.

A default makes the list non-simplePop out in the code editor (opens in a new tab)JavaScript
function total(price = 3) {  return price + 2;}console.log(total());

Line 1 gives price a default of 3, making the list non-simple. Line 2 adds 2; line 4 supplies no argument, so it prints 5. Default parameters themselves are valid. The combination below is the problem.

A body directive with non-simple parametersPop out in the code editor (opens in a new tab)JavaScript
console.log("before");
function total(price = 3) {
  "use strict";
  return price + 2;
}

Line 1 would print before. Line 2 has a default parameter; line 3 contains a body directive. Together they cause an early SyntaxError. Do not confuse a body directive with a function placed inside an already strict module: the specific error here is about the directive in that body and a non-simple list.

Duplicate arrow parametersPop out in the code editor (opens in a new tab)JavaScript
console.log("before");
const total = (price, price) => price;
Duplicate parameters with a strict bodyPop out in the code editor (opens in a new tab)JavaScript
console.log("before");
function total(price, price) {
  "use strict";
  return price;
}
Duplicate parameters with a defaultPop out in the code editor (opens in a new tab)JavaScript
console.log("before");
function total(price = 3, price) {
  return price;
}

Each failed source has a line 1 log that never happens. Arrow parameters must be distinct; strict function bodies forbid duplicates; non-simple parameter lists also require distinct names even without a body directive. A legacy non-strict simple function can differ, so do not teach "all duplicate parameters are invalid" as a blanket rule. Read the current function rules for the exact contexts.

[no LineTerminator here] is a real restriction

A restricted production forbids a line terminator at a marked position. After return, a line break can produce a bare return through automatic semicolon insertion (ASI). After throw, the same break cannot make a valid throw statement. These are different outcomes.

A return followed by a new linePop out in the code editor (opens in a new tab)JavaScript
function receipt() {  return  3;}console.log(receipt());

Line 1 starts a function. Line 2 returns without a value because the next line cannot be part of that return. Line 3 is a separate unreachable expression. Line 5 calls the function and prints undefined, not 3.

Throw cannot cross this line breakPop out in the code editor (opens in a new tab)JavaScript
console.log("before");
throw
new Error("no order");

Line 1 cannot print before. Line 2 has no expression before the line break; line 3 cannot repair it. Parsing fails with SyntaxError before evaluation begins. This is why adding semicolons mechanically is not a universal fix for newline bugs.

An arrow may not cross the marked breakPop out in the code editor (opens in a new tab)JavaScript
const total = (price)
  => price + 2;
console.log(total(3));

Line 1 closes the arrow parameters. Line 2 starts with => after a line break, which this production forbids. Line 3 never logs. The spec's notation for restricted productions makes the difference between ordinary white space and a forbidden line break explicit.

Check a checkout script before blaming a function

Suppose a checkout page stops rendering. Reduce its JavaScript to one valid read before inspecting application state. An early error anywhere in the same script prevents the first useful log, while a valid script can log and then fail at runtime.

A valid cart readPop 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 costs 3. Line 2 reads the field and prints 3. Next, a destructuring declaration can name that field price without declaring the field name itself.

Rename the field without creating a collisionPop out in the code editor (opens in a new tab)JavaScript
const cart = { tea: 3 };const { tea: price } = cart;console.log(price);

Line 1 creates the cart. Line 2 takes its tea value into the local price. Line 3 prints 3. Adding another let price in this scope would reject the script before any line runs, not merely replace the price.

More early errors are worth recognizing in reviews. An invalid assignment target such as 3 = 4, a break to an unknown label, and a regex literal with repeated g flags cannot start this script. The following sources deliberately fail; their first logs are probes for the no-output rule.

A number cannot be an assignment targetPop out in the code editor (opens in a new tab)JavaScript
console.log("before");
3 = 4;
A break needs a matching labelPop out in the code editor (opens in a new tab)JavaScript
console.log("before");
for (const order of ["tea"]) {
  break missingLabel;
}
A regex flag may not be repeatedPop out in the code editor (opens in a new tab)JavaScript
console.log("before");
const pattern = /tea/gg;

Line 1 of each failed source is the same probe. In the assignment case, line 2 has no valid target. In the label case, line 3 has no matching label; in the regex case, line 2 repeats the flag. Each fails before any log. For AST and parser implementation details, use Parsing and ASTs; this lesson asks what the language requires.

Source checks or runtime work?
  • throw followed by a line break
  • Two let price declarations in one scope
  • A regex literal with /tea/gg
  • Call missingPrice() after a log
  • Find the bound name in {tea: price}
  • Ask whether price = 3 is a simple parameter
Try it yourself
0 of 6 correct

Sort each case by the kind of question it answers. Read the reason on each card.

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

Common misconceptions

Syntax and static semantics both inspect source, but they answer different questions. Grammar asks whether the tokens fit; early errors ask whether an otherwise parsed form is allowed. A JavaScript error object alone is not enough to determine when it arose.

  • “The log before bad code still runs.” Not when the same script has an early error.
  • “Every SyntaxError is caught by try/catch in that source.” An early error prevents that catch from starting.
  • “Any parentheses before an arrow are ordinary grouping.” An arrow head must satisfy its supplemental parameter grammar.
  • “Static semantics are a slow engine phase.” They are specification rules, not a required implementation schedule.
  • “Every newline inserts a semicolon.” Return may split; a newline after throw makes the source invalid.
  • “Any duplicate name is invalid.” The relevant declaration kind, scope, and function context matter.

Compare the let error with the valid repeated var example, and compare the newline after return with the one after throw. The interesting question is not just whether the source contains a familiar keyword; it is which production and early-error rule applies there.

When a failure can occur
CaseWhyObservable effect
Grammar mismatchThe tokens cannot form the selected Script or Module goal.The source is rejected before that unit runs.
Early errorThe shape parses, but a static rule rejects it.The source is rejected before that unit runs.
Runtime errorValid source starts evaluating, then an operation throws.Earlier effects in the same script may already have happened.

Practice exercises

Predict the visible output first, then name the rule. Each starter has only a few lines; use Edit & run for complete valid examples and read deliberate failure examples without trying to recover them in the same script.

Exercise 1 · Warm-upRead a grouped value

A receipt stores a price in parentheses. Predict the log, then say why this is grouping and not arrow parameters.

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 2 · Warm-upTell when a call fails

    A checkout script logs before trying a missing function. Is this an early or runtime error? Include the first printed word in your answer.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    console.log("before");
    try {
      missingPrice();
    } catch (error) {
      console.log(error.name);
    }

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

      Exercise 3 · PracticeChoose the parenthesis reading

      A price updater uses an arrow. What more specific grammar must its parenthesized head cover? Test the completed function too.

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

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

        Exercise 4 · PracticeFind the bound name

        A cart has a tea property. Name the binding introduced by the destructuring pattern; do not answer with the property key.

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

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

          Exercise 5 · ChallengeCatch a bad function header

          The developer expects a log before the function declaration. Can this script start? State which two static questions decide the answer.

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          console.log("before");
          function total(price = 3) {
            "use strict";
            return price + 2;
          }

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

            Exercise 6 · ChallengeRepair a checkout script

            A real checkout page failed after someone added a second let price. Keep a single destructured price binding and predict the working output. Explain why the repaired script can run.

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

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

              Check your understanding

              For each code question, decide first whether the source can run at all. Then predict logs in order. For spec questions, separate the grammar's shape from named static rules and runtime behaviour.

              Syntactic grammar quiz · 8 questionsScore: first tries count
              1. Question 1 of 8What does one colon after a nonterminal signal?

                Choose an answer to see the explanation.

              2. Question 2 of 8What does this grouped expression print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const price = (3);
                console.log(price);

                Choose an answer to see the explanation.

              3. Question 3 of 8How many lines from this script run?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                console.log("before");
                let price = 3;
                let price = 4;

                Choose an answer to see the explanation.

              4. Question 4 of 8Which name does BoundNames find in const {tea: price} = cart?

                Choose an answer to see the explanation.

              5. Question 5 of 8Why does (price) => price + 2 use a cover grammar?

                Choose an answer to see the explanation.

              6. Question 6 of 8What does this function print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                function receipt() {
                  return
                  3;
                }
                console.log(receipt());

                Choose an answer to see the explanation.

              7. Question 7 of 8What does this valid script print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                console.log("before");
                try { missingPrice(); } catch (error) { console.log(error.name); }

                Choose an answer to see the explanation.

              8. Question 8 of 8Can a function with price = 3 parameters contain a use strict directive in its body?

                Choose an answer to see the explanation.

              Key takeaways

              A useful reading sequence is: find the grammar production, check its parameters and restrictions, then read the static rules. Only after that should you trace what evaluation prints or throws.

              • Syntactic productions combine tokens; grammatical parameters choose the context-specific forms.
              • Optional parts, lookahead, and restricted line breaks change which source is accepted.
              • Cover grammars allow an initial shape and then require a valid supplemental reading.
              • Early errors reject the whole script or module before its first evaluation.
              • BoundNames, VarDeclaredNames, LexicallyDeclaredNames, IsSimpleParameterList, and ContainsUseStrict describe source, not runtime values.
              • A runtime ReferenceError can follow an earlier log; an early SyntaxError cannot.

              Remember the one-liner.
              Grammar builds a source shape; static rules decide whether that shape is allowed before it runs.

              Coming next: Evaluation order. That lesson follows the work an accepted expression does from left to right. Meanwhile, Parsing and ASTs covers the engine view.

              CompleteFrontend Clear concepts. Working examples.