completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

Parsing & abstract syntax trees

Learn how JavaScript parsers turn tokens into ESTree-shaped ASTs, report early errors, handle cover grammars, and power tools.

By the end, you can
  • 01
    Follow the parserExplain how a parser made of small functions turns tokens into a tree, and how binding power decides which operator goes first.
  • 02
    Read ASTsRecognize common ESTree node types, and compare a small parser's tree with the tree from Acorn, a real parser.
  • 03
    Tell errors apartSay whether a problem is a syntax error, an early error, or a runtime error, and explain how a cover grammar waits for the next token.

The parser's job

Before JavaScript can run your code, it reads the text in steps. The scanner comes first. It cuts the text into tokens: small pieces such as a name, a number, or a + sign.

The parser comes next. It does three jobs with the tokens:

  • It checks that the tokens follow JavaScript's grammar: the rules for how code may be written.
  • It reports a SyntaxError when they don't.
  • It builds a tree that shows which pieces belong together.

The engine then turns that tree into bytecode: simple instructions that it can run. Tools such as linters and formatters read a similar tree to check, tidy, or change your code.

Plain definition

Parsing is the step that checks tokens against the grammar and turns them into a tree. An abstract syntax tree, or AST, is that tree. Each item in it is a node, such as "a call" or "an addition". The AST keeps the meaning of the code. It leaves out punctuation that is no longer needed, such as extra brackets.

The tokens a, +, b, *, c, (, d, ) go into the parser. Out comes a tree. The top node is +. Its left child is a. Its right child is *, and the children of * are b and the call c(d).tokensa+b*c(d)parser+treea*bc(d)
The parser reads the tokens and builds a tree. The * sits under the +, so b * c(d) is worked out first.

This lesson uses ideas from earlier lessons:

It also touches linting, destructuring, arrow functions, and strict mode.

Real-life analogyDiagramming sentences in grammar class

In a grammar class, you may have drawn a diagram of a sentence. It shows which words belong together: the adjective goes with its noun, and the noun goes with its verb. A parser draws the same kind of diagram for code. Its rules are strict. Every token must fit somewhere, or the parser stops with an error.

In real life: Words in a sentence
In JavaScript: Tokens, such as names, numbers, and +
In real life: Grammar rules
In JavaScript: JavaScript's grammar rules, such as what a statement looks like
In real life: A sentence diagram
In JavaScript: A tree that shows which pieces belong together
In real life: A tidy diagram without commas or full stops
In JavaScript: An AST, which keeps the meaning and drops extra punctuation

The table below lists four things that can appear between your text and the running code. Read each row from left to right: the name, what it is, an example, and who uses it.

Four things made on the way from your code to running it
What it is calledWhat it isExampleWho uses it
TokenA small piece of your text, sorted by the scanner.a, +, 2The parser reads tokens.
Parse tree (also called a concrete syntax tree)A tree that follows every grammar rule and keeps every bracket and comma.Brackets, commas, and every grammar stepThe spec and the inside of parsers.
Abstract syntax tree (AST)A smaller tree that keeps only the parts with meaning.BinaryExpression, CallExpressionLinters, formatters, and compilers.
BytecodeSimple instructions that the engine's interpreter runs.V8's Ignition bytecodeThe engine, after parsing.

A parser made of small functions

REAL PARSER

Most JavaScript parsers are recursive descent parsers. That means:

  • There is one function for each grammar rule. One function reads a statement; another reads an expression.
  • The functions call each other. The statement function calls the expression function when it needs one.
  • A function may call itself. That is the "recursive" part. It is how the parser handles things inside things, such as brackets inside brackets or a call inside a call.
  • The parser starts at the biggest rule, the whole program, and works down to smaller rules. That is the "descent" part.

The table compares four ways of parsing that this lesson mentions. Read each row as: the name, how it works, and what it suits best.

Four ways of parsing used in this lesson
Way of parsingHow it worksWhat it suits best
Recursive descentOne function for each grammar rule. The functions call each other.Statements, declarations, blocks, and simple values.
Precedence climbingOne loop for all operators. Each operator's binding power decides the order.Operators like + and *, and ** grouping from the right.
Pratt parsingA separate small handler for each kind of token.Languages with many kinds of operators.
Cover grammarAccept a broad shape first, then check what it turns out to be.(a, b), which may be an expression or arrow parameters.

Expressions need one more idea: precedence, which means which operator goes first. In a + b * c, the * goes first.

This lesson has its own small parser, called the mini parser. It gives each operator a number called its binding power. A bigger number means the operator holds the values beside it more tightly: + and - have 10, * and / have 20, and ** has 30.

The mini parser reads the value on the left. Then it keeps taking operators whose binding power is high enough. This way of parsing is called precedence climbing.

You may also hear of a Pratt parser. It is related, but it is not the same thing. A Pratt parser picks a separate small handler for each kind of token. The mini parser has just one loop for all operators like + and *, so "precedence climbing" is the right name for it.

Parse a + b * c(d) using binding power
Step 0 of 18Ready
Your turn: follow the blue line

Step through the real mini parser on a + b * c(d). Watch the call stack, and watch left and right change as binding power groups the tokens.

Running in
  1. script
Next: line 47
const input = "a + b * c(d)";const tokens = tokenize(input);let cursor = 0; function parseExpression(minBP = 0) {  let left = parsePostfix();  while (isBinary(peek()) && bindingPower(peek()) >= minBP) {    const operator = consume();    const right = parseExpression(nextMinBP(operator));    left = binary(left, operator, right);  }  return left;} function parsePostfix() {  let expression = parsePrimary();  while (peek() === "(" || peek() === ".") {    expression = parseCallOrMember(expression);  }  return expression;} function parsePrimary() {  return tokenToNode(consume());} function tokenize(source) {  return source.match(/[A-Za-z_$][A-Za-z0-9_$]*|[0-9]+|[*][*]|[()+*/.,-]/g).concat("<eof>");}function peek() { return tokens[cursor]; }function consume() { return tokens[cursor++]; }function isBinary(token) { return ["+", "-", "*", "/", "**"].includes(token); }function bindingPower(token) {  return token === "+" || token === "-" ? 10 : token === "*" || token === "/" ? 20 : token === "**" ? 30 : -1;}function nextMinBP(operator) { return operator === "**" ? bindingPower(operator) : bindingPower(operator) + 1; }function tokenToNode(token) { return /^[A-Za-z_$]/.test(token) ? { type: "Identifier", name: token } : { type: "Literal", value: Number(token), raw: token }; }function parseCallOrMember(expression) {  if (peek() === ".") return { type: "MemberExpression", object: expression, property: tokenToNode((consume(), consume())) };  consume();  const args = peek() === ")" ? [] : [parseExpression(0)];  consume();  return { type: "CallExpression", callee: expression, arguments: args };}function binary(left, operator, right) { return { type: "BinaryExpression", left, operator, right }; } 
CallStoreChangeResultBlue line: runs next. 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.

Step through the code one line at a time. Like a real parser, it has three main functions:

  • parseExpression (line 5) reads a whole expression, operators included.
  • parsePostfix (line 15) reads a value and anything stuck right after it, such as a call (d) or a dot .name. "Postfix" means "comes after".
  • parsePrimary (line 23) reads one simple token, such as a name or a number.

The recording comes from running the lesson's real parser on a + b * c(d).

Calls and dots join up before math operators
Step 0 of 10Ready
Your turn: follow the blue line

Step through a second run of the same parser. This time the tree grows in the call-and-dot loop. Calls and dots join up before any operator gets a turn.

Running in
  1. script
Next: line 47
const input = "a + b * c(d)";const tokens = tokenize(input);let cursor = 0; function parseExpression(minBP = 0) {  let left = parsePostfix();  while (isBinary(peek()) && bindingPower(peek()) >= minBP) {    const operator = consume();    const right = parseExpression(nextMinBP(operator));    left = binary(left, operator, right);  }  return left;} function parsePostfix() {  let expression = parsePrimary();  while (peek() === "(" || peek() === ".") {    expression = parseCallOrMember(expression);  }  return expression;} function parsePrimary() {  return tokenToNode(consume());} function tokenize(source) {  return source.match(/[A-Za-z_$][A-Za-z0-9_$]*|[0-9]+|[*][*]|[()+*/.,-]/g).concat("<eof>");}function peek() { return tokens[cursor]; }function consume() { return tokens[cursor++]; }function isBinary(token) { return ["+", "-", "*", "/", "**"].includes(token); }function bindingPower(token) {  return token === "+" || token === "-" ? 10 : token === "*" || token === "/" ? 20 : token === "**" ? 30 : -1;}function nextMinBP(operator) { return operator === "**" ? bindingPower(operator) : bindingPower(operator) + 1; }function tokenToNode(token) { return /^[A-Za-z_$]/.test(token) ? { type: "Identifier", name: token } : { type: "Literal", value: Number(token), raw: token }; }function parseCallOrMember(expression) {  if (peek() === ".") return { type: "MemberExpression", object: expression, property: tokenToNode((consume(), consume())) };  consume();  const args = peek() === ")" ? [] : [parseExpression(0)];  consume();  return { type: "CallExpression", callee: expression, arguments: args };}function binary(left, operator, right) { return { type: "BinaryExpression", left, operator, right }; } 
CallStoreChangeResultBlue line: runs next. 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 second replay parses config.get(user.name).theme. It shows that the calls and dots join up first, into one chain. Only after that do operators such as + or * get a turn. Calls and dots attach straight onto the value before them.

Two kinds of tree, and the ESTree format

NODE SHAPES

There are two kinds of syntax tree:

  • A concrete syntax tree, also called a parse tree, follows the grammar rules exactly. It keeps every bracket, every comma, and every in-between step. That helps while parsing, but it is noisy for most tools.
  • An abstract syntax tree (AST) keeps only the parts that carry meaning.

ESTree is a shared agreement about what a JavaScript AST should look like: the names of the nodes and the names of their fields. Many tools follow it. Acorn and Espree (two parsers), AST Explorer, ESLint rules, and many Babel-compatible tools give you ESTree nodes, or nodes very close to them.

The V8 engine, used in Chrome and Node.js, builds an AST too. V8's article about its scanner says it then turns that AST into bytecode for Ignition, V8's interpreter. But V8's own nodes are not ESTree nodes.

Each row below is one node type you will often see. The last column lists the fields that hold its parts.

Common ESTree node types
Node typeWhat it stands forIts main fields
ProgramThe whole script or module.body (a list of statements), sourceType
VariableDeclarationA let, const, or var line.kind, declarations
BinaryExpressionAn operator between two values, such as a + b.left, operator, right
CallExpressionA call to a function or method.callee (what is called), arguments, optional
MemberExpressionReading a property, such as obj.name.object, property, computed
ArrowFunctionExpressionAn arrow function, once the parser knows it is one.params, body, expression

The panel below parses the same line of code twice: once with the mini parser and once with Acorn. Pick a line and compare the two trees.

The mini parser's tree next to Acorn's tree
Mini parsersame
{
  "type": "Program",
  "body": [
    {
      "type": "ExpressionStatement",
      "expression": {
        "type": "BinaryExpression",
        "left": {
          "type": "Identifier",
          "name": "a"
        },
        "operator": "+",
        "right": {
          "type": "BinaryExpression",
          "left": {
            "type": "Identifier",
            "name": "b"
          },
          "operator": "*",
          "right": {
            "type": "CallExpression",
            "callee": {
              "type": "Identifier",
              "name": "c"
            },
            "arguments": [
              {
                "type": "Identifier",
                "name": "d"
              }
            ],
            "optional": false
          }
        }
      }
    }
  ],
  "sourceType": "script"
}
AcornESTree
{
  "type": "Program",
  "body": [
    {
      "type": "ExpressionStatement",
      "expression": {
        "type": "BinaryExpression",
        "left": {
          "type": "Identifier",
          "name": "a"
        },
        "operator": "+",
        "right": {
          "type": "BinaryExpression",
          "left": {
            "type": "Identifier",
            "name": "b"
          },
          "operator": "*",
          "right": {
            "type": "CallExpression",
            "callee": {
              "type": "Identifier",
              "name": "c"
            },
            "arguments": [
              {
                "type": "Identifier",
                "name": "d"
              }
            ],
            "optional": false
          }
        }
      }
    }
  ],
  "sourceType": "script"
}
Try it yourself

With the positions removed, the two trees are the same for this code.

Positions are removed before comparing, because parsers differ in how much position detail they add. The node types and what sits inside what must still match.
ESTree is for tools, not for the engine

V8's scanner article says V8 parses code into an AST and then turns that AST into Ignition bytecode. The ESTree project is a format that tools agreed to share. Both are ASTs, but they are not the same format.

Which operator goes first, and one surprising SyntaxError

REAL OUTPUT

The operator lesson listed which operators go first. The parser turns that list into the shape of the tree. The deeper a node sits, the earlier it is worked out:

  • * sits deeper than +.
  • A dot, as in user.name, sits deeper than *.
  • ** ("to the power of") groups from the right. This is called being right-associative. So 2 ** 3 ** 2 means 2 ** (3 ** 2).
** groups from the right, and -2 ** 2 is refusedPop out in the code editor (opens in a new tab)JavaScript
console.log(2 ** 3 ** 2); try {  new Function("return -2 ** 2;");  console.log("parsed");} catch (error) {  console.log(error.name + ": " + error.message);} console.log(2 ** -2);console.log((-2) ** 2);console.log(-(2 ** 2));

Line 1 prints 512. JavaScript reads 2 ** 3 ** 2 as 2 ** (3 ** 2). That is 2 ** 9, which is 512.

Line 4 gives the string "return -2 ** 2;" to new Function. new Function turns a string into a function. It must parse the string first, and here the function is never called.

A unary operator works on one value, like the minus in -2. The grammar has a special rule: a unary operator may not sit right before **. So parsing fails, and line 7 prints the SyntaxError and its message.

Left: 2 ** 3 ** 2 becomes a tree. The top ** has 2 on the left and another ** on the right, which holds 3 and 2. It prints 512. Right: -2 ** 2 has a minus right before **, which is not allowed. It is a SyntaxError.2 ** 3 ** 2**2**32prints 512-2 ** 2-2**2minus right before **is not allowedSyntaxError
** groups from the right, so 3 ** 2 is worked out first and the answer is 512. A minus sign right before ** is not allowed, so -2 ** 2 never runs.

Why the rule? Some people read -2 ** 2 as (-2) ** 2, which is 4. Others read it as -(2 ** 2), which is -4. JavaScript refuses to guess, so you write the brackets yourself.

Lines 10 to 12 show forms that work. A minus on the right side is fine, so 2 ** -2 prints 0.25. (-2) ** 2 prints 4, and -(2 ** 2) prints -4.

Early errors stop the whole script

PARSE VS RUN

Most errors happen while code runs. Those are runtime errors. When one happens, the lines before it have already run.

The ECMAScript specification, the official rulebook for JavaScript, also lists early errors. An early error is found before any of the code runs. If a script has one, not a single line of that script runs. The same goes for a module, or a string passed to eval. This is not the engine guessing that code will fail later. The spec names each early error exactly.

The code below uses eval. It takes a string, then parses and runs it as code. Each string is parsed on its own, so you can see what happens to it.

An early error stops everything; a runtime error stops partwayPop out in the code editor (opens in a new tab)JavaScript
const early = [];try {  eval('early.push("before"); let item = 1; let item = 2; early.push("after");');} catch (error) {  early.push(error.name);}console.log(early.join(" -> ")); const runtime = [];try {  eval('runtime.push("before"); missingFunction(); runtime.push("after");');} catch (error) {  runtime.push(error.name);}console.log(runtime.join(" -> "));

Line 3: the first string declares let item twice. That is an early error. So the early.push("before") at the start of the string never runs. Line 7 prints only SyntaxError.

Line 11: the second string follows the grammar, so it starts running. It pushes "before". Then it fails at missingFunction(), a name that doesn't exist. Line 15 prints before -> ReferenceError.

Left console: the code with let item twice prints only SyntaxError, because nothing ran. Right console: the code that calls missingFunction prints before -> ReferenceError, because the first line ran.let item twiceconsoleSyntaxErrornothing ranmissingFunction()consolebefore -> ReferenceErrorthe first line ran
On the left, the parser finds the error before any line runs, so before is never added. On the right, the code starts running and fails partway.

Here are some common early errors to recognize. They are separate examples, so don't run them as one file.

Common early errors to recognizeJavaScript
// Separate examples to read. Don't run them together.let total = 1;let total = 2;       // the same let name twice return 1;            // return outside a function1 = 2;               // you can't assign to a number "use strict";010;                 // an old-style octal number in strict code

The next code checks six early errors, each in its own string:

  • the same let name twice
  • return outside a function
  • assigning to something that can't hold a value, such as 1
  • an old-style octal number such as 010 in strict mode
  • two parameters with the same name in strict mode
  • await outside an async function, in a normal script

Each eval fails before it runs anything, so every line prints SyntaxError.

Check six early errors, one string at a timePop out in the code editor (opens in a new tab)JavaScript
const cases = [  ["duplicate let", 'let count; let count;'],  ["return outside function", 'return 1;'],  ["invalid assignment target", '1 = 2;'],  ["strict octal literal", '"use strict"; 010;'],  ["strict duplicate parameters", '"use strict"; function f(a, a) {}'],  ["await outside async", 'await 1;'],]; for (const [label, source] of cases) {  try {    eval(source);    console.log(label + ": ran");  } catch (error) {    console.log(label + ": " + error.name);  }}
Early error or runtime error?
  • `let count; let count;`
  • `console.log('before'); missingFunction();`
  • `1 = 2;`
  • `'use strict'; 010;`
  • `console.log('before'); throw new Error('boom');`
  • `const add = (a, b) => a + b;`
Try it yourself
0 of 6 correct

Sort each snippet by when JavaScript reports the problem. Remember: an early error stops every line of that script or eval string from running.

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

Cover grammars: when the parser must wait

AMBIGUITY

Sometimes the parser can't tell what some tokens mean until it sees a later token. Take (a, b):

  • If no => follows, it is a comma expression in brackets. It works out a, then b, and gives back b.
  • If => follows, the same tokens are the parameters of an arrow function.

The spec handles this with a cover grammar. That is a broad rule that "covers" both meanings for a while. When the later token arrives, the parser checks the tokens against the rules for the meaning that is now clear. It doesn't run any code, and it doesn't have to read the text twice.

The tokens (a, b) can become two things. If no arrow follows, as in (a, b);, it is a comma expression. If an arrow follows, as in (a, b) => a + b, it is a list of arrow function parameters.(a, b)no => next=> next(a, b);(a, b) => a + bcomma expressionarrow parameters
The parser can't tell what (a, b) is yet. The next token decides: no arrow means a comma expression, an arrow => means parameters.

Each row below shows a piece of code, what it ends up as, and what decides that.

One shape, several final meanings
The codeWhat it ends up asWhat decides
(a, b)A comma expression in brackets.No => comes after it.
(a, b) => a + bThe parameters of an arrow function.The parser then sees =>.
({ a } = b)A destructuring pattern that takes b.a and stores it in a.The object-looking shape sits to the left of =.
({ a: 1 })A normal object.The brackets make it a value, and no = follows.
Four lookalike shapes that all parsePop out in the code editor (opens in a new tab)JavaScript
const samples = [  ["parenthesized expression", "let a = 1, b = 2; (a, b);"],  ["arrow parameters", "const fn = (a, b) => a + b;"],  ["object assignment pattern", "let a; const b = { a: 2 }; ({ a } = b);"],  ["object literal expression", "const value = ({ a: 1 });"],]; for (const [label, source] of samples) {  try {    new Function(source);    console.log(label + ": parses");  } catch (error) {    console.log(label + ": " + error.name);  }}

Lines 2 to 5 are fixed strings. Line 11 hands each one to new Function, which parses it without running it. If there is no error, line 12 prints parses. All four print parses.

Objects have the same problem. In ({ a } = b), the part that looks like an object is really a destructuring pattern. It takes b.a and stores it in a. In a normal object, { a = 1 } is not allowed. So the parser accepts the broad shape first. Then it applies the rules for whatever the shape turns out to be.

Cover grammars are not the same as lazy parsing

V8 sometimes skips the inside of a function at first and parses it later. This is called lazy parsing. Its quick first pass is called the preparser.

V8's lazy parsing post explains that the preparser must handle a start like ({ d }) correctly. It could end as destructuring that reads an outer d. Or it could end as arrow parameters that make a new d. Getting this right is about keeping track of variables while preparsing.

Today V8 also saves notes about each function's variables, so a later full parse can skip inner functions. Older V8 versions preparsed deeply nested functions again and again. That speed trick is a separate topic from cover grammars in the spec.

Look at real ASTs

LIVE ACORN

The fastest way to learn ASTs is to change some code and watch the tree change. AST Explorer is the usual website for this. You pick a parser, type some code, and see its tree.

With Acorn, Espree, or a Babel-style parser, these options matter most:

  • ecmaVersion: which version of JavaScript to accept.
  • sourceType: "script" or "module". Only a module may use import at the top level.
  • locations: adds a line and column number to each node.
  • ranges or tokens: add exact character positions, for tools that highlight or change code.
Live AST explorer, using Acorn
Your code, with the picked part highlighted0..114
const total = prices
  .map((price) => price * tax)
  .reduce((sum, value) => sum + value, 0);
console.log(total);
AST treeAcorn
  • sourceType: "module"
    • kind: "const"
        • name: "total"
        • optional: false
          • computed: falseoptional: false
            • optional: false
              • computed: falseoptional: false
                • name: "prices"
                • name: "map"
              • id: nullexpression: truegenerator: falseasync: false
                • name: "price"
                • operator: "*"
                  • name: "price"
                  • name: "tax"
            • name: "reduce"
          • id: nullexpression: truegenerator: falseasync: false
            • name: "sum"
            • name: "value"
            • operator: "+"
              • name: "sum"
              • name: "value"
          • value: 0raw: "0"
      • optional: false
        • computed: falseoptional: false
          • name: "console"
          • name: "log"
        • name: "total"
Try it yourself

Acorn parsed the code. Open the nodes in the tree, and click one to highlight the exact text it came from.

Acorn parses your code in the browser. It never runs the code you type. Positions count characters from the start, and errors show Acorn's line and column.

Try these: delete a closing bracket ), add an import line, or click a CallExpression node. The playground parses your code with Acorn. It highlights the part of the text that each node came from. It never runs your code.

Where frontend tools use ASTs

WALK NODES

Tools use ASTs because they need to know what each piece of code is. A plain text search can't tell. For example:

  • A linter should flag a real console.log() call, but not the string "console.log".
  • A formatter must know what sits inside what before it decides where to break lines.
  • A codemod, a script that changes code across many files, should rename a function call without touching comments.

Each row below names a tool and what it does with the AST.

Tools that use ASTs every day
ToolWhat it does with the AST
ESLintParses your code, visits the nodes, and reports problems such as unused variables or console.log calls.
PrettierParses your code into an AST, then prints it again in one tidy layout.
Babel and SWCParse your code, change the nodes, and write new JavaScript for the browsers you support.
BundlersRead the import and export nodes, work out which files need which, and join them into bundles.
CodemodsFind old code patterns in the tree and change them safely across many files.

The panel below uses acorn-walk, a helper that visits the nodes of an Acorn tree. It has two ways to walk:

  • full walks the whole tree and calls your function on each node it visits. It skips the name after a dot, such as map in prices.map. So the code below counts 7 names, not 9.
  • ancestor calls your function only for the node types you name, such as CallExpression. It also tells you the nodes above it.
Walk an AST to count names and find console.log calls
Count names and find console.logJavaScript
import { parse } from "acorn";import { ancestor, full } from "acorn-walk"; const source = `const total = prices.map((price) => price * tax);console.log(total);`;const ast = parse(source, { ecmaVersion: 2024, locations: true }); let identifiers = 0;full(ast, (node) => {  if (node.type === "Identifier") identifiers += 1;}); const consoleLogs = [];ancestor(ast, {  CallExpression(node) {    if (      node.callee.type === "MemberExpression" &&      node.callee.object.type === "Identifier" &&      node.callee.object.name === "console" &&      node.callee.property.type === "Identifier" &&      node.callee.property.name === "log"    ) {      consoleLogs.push(node.loc.start.line);    }  }}); console.log(identifiers);console.log(consoleLogs.join(","));
Outputverified
Identifier nodes (names)7
Lines with console.log2
Try it yourself
Real `acorn-walk` result

The helper parses the code. It counts every Identifier node with full. Then it uses ancestor to find a CallExpression that calls console.log.

You can't run this code here, because it imports packages from the project. The output beside it comes from the lesson's real helper, and the tests check it.

The bundlers lesson used Acorn in the same way to find import lines. The transpilers lesson used Acorn's positions to rewrite ??. Once code is a tree, a tool can make exact changes that a text search can't.

Common misconceptions

  • "Grammar and syntax are the same word." Grammar is the set of rules. Syntax is the code you actually wrote, which the parser checks against those rules.
  • "An AST holds every token." A parse tree keeps every grammar detail. An AST keeps a smaller tree with only the parts that carry meaning.
  • "ESTree is what the engine runs." ESTree is a format for tools. Engines build their own private ASTs and turn them into bytecode or machine code.
  • "An early error happens after the lines before it run." No. An early error stops every line of the script, module, or eval string from running.
  • "Cover grammars mean the parser guesses while the code runs." No. It all happens while parsing. A later token decides what the broad shape must be. No code runs while it decides.

Each row below is a pair of similar words. Read across: how they differ, then a safe way to say it.

Words that are easy to mix up when talking about parsers
The two wordsHow they differA safe way to say it
Syntax vs grammarSyntax is the code you wrote. Grammar is the set of rules that accepts or rejects it.Your code has syntax; the parser checks it against the grammar.
Parse tree vs ASTA parse tree keeps every grammar step and bracket. An AST keeps only the parts with meaning.Tools usually give you an AST.
Early error vs runtime errorAn early error is found before any line runs. A runtime error happens when the code reaches a bad line.Put the code in a fixed eval string to see which one it is.
ESTree vs V8's ASTESTree is a shared format for tools. V8's AST is private to the engine.Call Acorn's output ESTree-shaped.

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upPredict the top node of the tree

The code holds a cut-down AST for a + b * c(d). Use the mini parser's binding powers to predict the top node's type and operator.

Starter codePop out in the code editor (opens in a new tab)JavaScript
const ast = {
  body: [{ expression: { type: "BinaryExpression", operator: "+" } }]
};
console.log(ast.body[0].expression.type);
console.log(ast.body[0].expression.operator);

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

    Exercise 2 · PracticeAdd unary minus to the parser

    What ESTree node type should stand for -x once it is parsed?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    // Add a parseUnary function that makes this node:
    // { type: "UnaryExpression", operator: "-", argument, prefix: true }
    // Read the value after the minus with parseUnary(), not parseExpression(30).
    // The operator loop must refuse ** when its unparenthesized left node is unary.

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

      Exercise 3 · PracticeWrite an acorn-walk visitor

      A visitor is an object of functions, one per node type, that the walker calls. Which node type should your visitor look at first to find real console.log(...) calls?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      function isConsoleLogCall(node) {
        const callee = node.callee;
        return callee.type === "MemberExpression" &&
          callee.object.name === "console" &&
          callee.property.name === "log";
      }
      console.log(isConsoleLogCall({
        callee: { type: "MemberExpression", object: { name: "console" }, property: { name: "log" } }
      }));

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

        Exercise 4 · PracticeSpot the early error

        Does the first console.log run before JavaScript reports the second let total?

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        console.log("before");
        let total = 1;
        let total = 2;
        console.log("after");

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

          Exercise 5 · ChallengeCompare the mini parser with Acorn

          The code stands in for two trees with their positions removed. For code the mini parser handles, what should the comparison print?

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          const mini = { type: "Program", body: ["same"], sourceType: "script" };
          const acorn = { type: "Program", body: ["same"], sourceType: "script" };
          console.log(JSON.stringify(mini) === JSON.stringify(acorn));

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

            Check your understanding

            8 QUESTIONS
            Lesson quiz · 8 questionsScore: first tries count
            1. Question 1 of 8What does a JavaScript parser take in, and what does it give back?

              Choose an answer to see the explanation.

            2. Question 2 of 8Why does the mini parser put * under + in the tree for a + b * c(d)?

              Choose an answer to see the explanation.

            3. Question 3 of 8What does this line print?

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

              Choose an answer to see the explanation.

            4. Question 4 of 8Which sentence about ESTree is true?

              Choose an answer to see the explanation.

            5. Question 5 of 8This code puts an early error inside eval. What does it print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const events = [];
              try {
                eval('events.push("before"); let x; let x;');
              } catch (error) {
                events.push(error.name);
              }
              console.log(events.join(" -> "));

              Choose an answer to see the explanation.

            6. Question 6 of 8Why is (a, b) a cover grammar shape?

              Choose an answer to see the explanation.

            7. Question 7 of 8Which option tells Acorn whether import is allowed at the top level?

              Choose an answer to see the explanation.

            8. Question 8 of 8Which of these works with an AST?

              Choose an answer to see the explanation.

            Key takeaways

            • The parser reads tokens, checks them against the grammar, reports syntax errors and early errors, and builds a tree.
            • A recursive descent parser has one function per grammar rule. This lesson's operator loop is called precedence climbing.
            • ESTree is a shared AST format for tools. It is not V8's own AST.
            • An early error stops every line of its script, module, or eval string from running.
            • A cover grammar accepts a broad shape first. A later token decides what it must be, and the parser checks it then.
            • ASTs power linters, formatters, code changers, bundlers, and clear syntax error messages.

            Remember the one-liner.
            Parsing is where JavaScript stops being a row of tokens and becomes a checked tree that engines and tools can work with.

            Up next: Lazy parsing & preparsing. There you'll see how engines decide how much code to parse at startup, and how much to leave for later.

            CompleteFrontend Clear concepts. Working examples.