Parsing & abstract syntax trees
Learn how JavaScript parsers turn tokens into ESTree-shaped ASTs, report early errors, handle cover grammars, and power tools.
- 01Follow the parserExplain how a parser made of small functions turns tokens into a tree, and how binding power decides which operator goes first.
- 02Read ASTsRecognize common ESTree node types, and compare a small parser's tree with the tree from Acorn, a real parser.
- 03Tell 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
SyntaxErrorwhen 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.
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.
* sits under the +, so b * c(d) is worked out first.This lesson uses ideas from earlier lessons:
- Code structure: statements and expressions.
- Other operators: the list of which operator goes first.
- Bundlers: there, Acorn (a parser written in JavaScript) found the
importlines. - Transpilers & polyfills: there, tools changed code by changing its tree.
It also touches linting, destructuring, arrow functions, and strict mode.
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.
| What it is called | What it is | Example | Who uses it |
|---|---|---|---|
| Token | A small piece of your text, sorted by the scanner. | a, +, 2 | The 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 step | The spec and the inside of parsers. |
| Abstract syntax tree (AST) | A smaller tree that keeps only the parts with meaning. | BinaryExpression, CallExpression | Linters, formatters, and compilers. |
| Bytecode | Simple instructions that the engine's interpreter runs. | V8's Ignition bytecode | The engine, after parsing. |
A parser made of small functions
REAL PARSERMost 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.
| Way of parsing | How it works | What it suits best |
|---|---|---|
| Recursive descent | One function for each grammar rule. The functions call each other. | Statements, declarations, blocks, and simple values. |
| Precedence climbing | One loop for all operators. Each operator's binding power decides the order. | Operators like + and *, and ** grouping from the right. |
| Pratt parsing | A separate small handler for each kind of token. | Languages with many kinds of operators. |
| Cover grammar | Accept 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.
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.
script
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 }; } 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).
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.
script
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 }; } 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 SHAPESThere 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.
| Node type | What it stands for | Its main fields |
|---|---|---|
Program | The whole script or module. | body (a list of statements), sourceType |
VariableDeclaration | A let, const, or var line. | kind, declarations |
BinaryExpression | An operator between two values, such as a + b. | left, operator, right |
CallExpression | A call to a function or method. | callee (what is called), arguments, optional |
MemberExpression | Reading a property, such as obj.name. | object, property, computed |
ArrowFunctionExpression | An 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.
{
"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"
}{
"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"
}With the positions removed, the two trees are the same for this code.
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 OUTPUTThe 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. So2 ** 3 ** 2means2 ** (3 ** 2).
** groups from the right, and -2 ** 2 is refusedconsole.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.
** 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 RUNMost 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.
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.
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.
// 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 codeThe next code checks six early errors, each in its own string:
- the same
letname twice returnoutside a function- assigning to something that can't hold a value, such as
1 - an old-style octal number such as
010in strict mode - two parameters with the same name in strict mode
awaitoutside anasyncfunction, in a normal script
Each eval fails before it runs anything, so every line prints SyntaxError.
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); }}`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;`
Sort each snippet by when JavaScript reports the problem. Remember: an early error stops every line of that script or eval string from running.
Cover grammars: when the parser must wait
AMBIGUITYSometimes 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 outa, thenb, and gives backb. - 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.
(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.
| The code | What it ends up as | What decides |
|---|---|---|
(a, b) | A comma expression in brackets. | No => comes after it. |
(a, b) => a + b | The 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. |
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.
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 ACORNThe 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 useimportat the top level.locations: adds a line and column number to each node.rangesortokens: add exact character positions, for tools that highlight or change code.
const total = prices
.map((price) => price * tax)
.reduce((sum, value) => sum + value, 0);
console.log(total);sourceType: "module"kind: "const"name: "total"optional: falsecomputed: falseoptional: falseoptional: falsecomputed: falseoptional: falsename: "prices"name: "map"
id: nullexpression: truegenerator: falseasync: falsename: "price"operator: "*"name: "price"name: "tax"
name: "reduce"
id: nullexpression: truegenerator: falseasync: falsename: "sum"name: "value"operator: "+"name: "sum"name: "value"
value: 0raw: "0"
optional: falsecomputed: falseoptional: falsename: "console"name: "log"
name: "total"
Acorn parsed the code. Open the nodes in the tree, and click one to highlight the exact text it came from.
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 NODESTools 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.
| Tool | What it does with the AST |
|---|---|
| ESLint | Parses your code, visits the nodes, and reports problems such as unused variables or console.log calls. |
| Prettier | Parses your code into an AST, then prints it again in one tidy layout. |
| Babel and SWC | Parse your code, change the nodes, and write new JavaScript for the browsers you support. |
| Bundlers | Read the import and export nodes, work out which files need which, and join them into bundles. |
| Codemods | Find 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:
fullwalks the whole tree and calls your function on each node it visits. It skips the name after a dot, such asmapinprices.map. So the code below counts 7 names, not 9.ancestorcalls your function only for the node types you name, such asCallExpression. It also tells you the nodes above it.
console.logJavaScriptimport { 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(","));72The helper parses the code. It counts every Identifier node with full. Then it uses ancestor to find a CallExpression that calls console.log.
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
evalstring 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.
| The two words | How they differ | A safe way to say it |
|---|---|---|
| Syntax vs grammar | Syntax 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 AST | A 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 error | An 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 AST | ESTree is a shared format for tools. V8's AST is private to the engine. | Call Acorn's output ESTree-shaped. |
Practice exercises
5 EXERCISESThe 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.
const ast = {
body: [{ expression: { type: "BinaryExpression", operator: "+" } }]
};
console.log(ast.body[0].expression.type);
console.log(ast.body[0].expression.operator);The top node is a BinaryExpression for +. So the two lines print BinaryExpression and +.
What ESTree node type should stand for -x once it is parsed?
// 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.function parseUnary() {
if (peek().value !== "-") return parsePostfix();
consume();
return { type: "UnaryExpression", operator: "-", argument: parseUnary(), prefix: true };
}
if (peek().value === "**" && left.type === "UnaryExpression" && !parenthesized.has(left)) {
throw new SyntaxError("Parenthesize unary expressions before **");
}A minus in front makes a UnaryExpression. Don't read its value with parseExpression(30): that would also take the ** and wrongly accept -2 ** 2 as -(2 ** 2). So parseUnary calls itself for the value instead. The parser also remembers which nodes had brackets, so (-2) ** 2 still works.
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?
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" } }
}));ancestor(ast, {
CallExpression(node) {
const callee = node.callee;
if (callee.type === "MemberExpression") {
// check console.log here
}
}
});Start from CallExpression nodes. console.log only matters when it is called, not when it is just read.
Does the first console.log run before JavaScript reports the second let total?
console.log("before");
let total = 1;
let total = 2;
console.log("after");Nothing prints. The second let total is an early SyntaxError, so the script never runs the first console.log.
The code stands in for two trees with their positions removed. For code the mini parser handles, what should the comparison print?
const mini = { type: "Program", body: ["same"], sourceType: "script" };
const acorn = { type: "Program", body: ["same"], sourceType: "script" };
console.log(JSON.stringify(mini) === JSON.stringify(acorn));It prints true. Once the positions are removed, the mini parser's tree for config.get(user.name).theme is the same as Acorn's.
Check your understanding
8 QUESTIONSQuestion 1 of 8What does a JavaScript parser take in, and what does it give back?
Choose an answer to see the explanation.
Question 2 of 8Why does the mini parser put
*under+in the tree fora + b * c(d)?Choose an answer to see the explanation.
Question 3 of 8What does this line print?
Read the code, then predictconsole.log(2 ** 3 ** 2);Choose an answer to see the explanation.
Question 4 of 8Which sentence about ESTree is true?
Choose an answer to see the explanation.
Question 5 of 8This code puts an early error inside
eval. What does it print?Read the code, then predictconst 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.
Question 6 of 8Why is
(a, b)a cover grammar shape?Choose an answer to see the explanation.
Question 7 of 8Which option tells Acorn whether
importis allowed at the top level?Choose an answer to see the explanation.
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
evalstring 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.