Learn JavaScript

Lesson 4 of 6 ¡ What JavaScript Is, Where It Runs, and How an Engine Runs It

Module 0 ¡ What JavaScript Is, Where It Runs, and How an Engine Runs It

How the Engine Runs Your Code: Parse, Bytecode, JIT

FreeReading

In this lesson

  • Name three JavaScript engines and the browsers or runtimes they live in.
  • Follow one line of code from text to a parse tree, to bytecode, to machine code.
  • Read a SyntaxError and a ReferenceError and say which stage threw each one.

David writes a ten-line program that prints a small sales report. The first line is console.log("Starting the report..."), so he expects at least that to appear. He presses Run, and nothing prints at all. The Playground says "Compile error" and shows SyntaxError: Unexpected token ')', pointing at line 10.

Line 10 is the last line. How can the last line stop the first one from running? The engine had not even reached line 10 yet. Or had it?

It had. Before an engine runs a single line, it reads the whole file. This lesson follows a line of code through everything the engine does to it. By the end, David's empty output makes sense, and so will most error messages you meet.

What an engine is

An engine is the program that reads JavaScript text and runs it. Lesson 02 called it the box in the middle of the four sockets. There are three engines that matter, each made by a different organisation.

EngineMade byWhere you meet it
V8GoogleChrome, Edge, Node.js, Deno, and the Playground
SpiderMonkeyMozillaFirefox
JavaScriptCoreAppleSafari, and the Bun runtime

All three follow the same rulebook, ECMAScript, so your code behaves the same in each. Inside, they are built differently and name their parts differently. This lesson uses V8's names, because V8 runs your Playground code.

So an engine is a program, and three of them run the JavaScript in nearly every browser and server.

Stage 1: the parser reads the whole file first

The first part of the engine is the parser. It reads your text and checks that it follows the grammar of JavaScript. It does this for the whole file before anything runs.

The parser works in two small steps. First it cuts the text into tokens, the words and symbols of the language: return, price, *, count, ;. Then it arranges the tokens into a parse tree, a picture of what the line means.

The parse tree of return price * count; return price * count; Return Multiply ( * ) price count read top down: give back the product of two names
Figure 1. Five tokens become one small tree. The tree, not the text, is what the rest of the engine works from.

If the tokens cannot form a valid tree anywhere in the file, the parser stops and reports a SyntaxError. A syntax error means "this is not grammatical JavaScript". Since nothing has run yet, nothing prints. That is exactly what happened to David.

So a SyntaxError comes from stage 1, and it stops the whole file, not just its own line.

Stage 2: bytecode, run by an interpreter

Next, the engine turns the tree into bytecode: a list of tiny, simple instructions, much smaller than JavaScript statements. V8's part that makes and runs bytecode is called Ignition. It is an interpreter, a program that carries out instructions one at a time.

You can see real bytecode. Node.js can print it with a flag, on your own machine. This is what Node 22 printed for the line in Figure 1, inside the small function of Example 1 below:

Ldar a1
Mul a0, [0]
Return

Three instructions. Ldar a1 loads the second argument, count, into a working slot called the accumulator. Mul a0, [0] multiplies it by the first argument, price. Return gives the result back. The [0] points to a slot where the engine notes which types it saw, which matters in stage 3.

The widget below steps one line through the whole journey. Press the step button to move from one stage to the next.

The line return price * count; goes through five stages, in order:

  1. Source text: return price * count;, plain text the engine has not read yet.
  2. Tokens: return, price, *, count, ;.
  3. Parse tree: Return, holding Multiply, holding price and count. A SyntaxError would stop everything here.
  4. Bytecode: Ldar a1, Mul a0, [0], Return, run one by one by Ignition.
  5. Machine code: after many calls, TurboFan compiles the busy function for the processor.

So bytecode is the engine's own short instruction list, and the interpreter runs it straight away.

Stage 3: the JIT compiles the busy code

Running bytecode one instruction at a time is quick to start but not the fastest way to run. A processor runs its own machine code much faster. So V8 watches which functions run often. When one gets "hot", a second part, the optimising compiler called TurboFan, turns it into machine code.

That is what JIT means: just-in-time compilation. The engine compiles code while the program runs, and only the code that is worth it. V8 also has two middle tiers, Sparkplug and Maglev, between Ignition and TurboFan. Other engines have similar tiers under other names.

TurboFan makes guesses to go fast. If price and count were always numbers, it builds machine code for numbers. If a string suddenly arrives, the engine throws that machine code away and goes back to bytecode. Your program still gives the right answer; it only runs slower for a moment.

The pipeline inside V8 Source text Parser parse tree Ignition bytecode, runs it TurboFan machine code only hot code back to bytecode when a guess is wrong SyntaxError ReferenceError A SyntaxError comes from the parser, before anything runs. A ReferenceError comes while the code runs.
Figure 2. V8's pipeline, and where the two errors of this lesson come from.

So the JIT is a compiler that works during the run, and only on the code that runs often.

Interpreted or compiled? Both, and both half right

You will read "JavaScript is an interpreted language" and also "JavaScript is compiled". Each is half true. Every line is interpreted first as bytecode. The busy parts are later compiled to machine code. Neither label describes the whole engine.

The useful difference is when the compiling happens. A C compiler turns the whole program into machine code before it runs, once. A JavaScript engine starts running at once and compiles the hot parts during the run. That is why a JavaScript program starts instantly and then speeds up.

If you did the C track

C works the other way round. Its four stages (preprocessor, compiler, assembler, linker) all happen before the program runs, and they produce a file of machine code. JavaScript keeps no such file: the engine parses and compiles in memory, every time the program starts. The C track's compile error and the Playground's "Compile error" for JavaScript are cousins: both come from reading the text before running it.

So JavaScript is neither "just interpreted" nor "compiled ahead of time"; it is compiled just in time.

Two errors, two stages

Every error message names its class first. Two classes are worth placing on the pipeline today, because they come from different stages.

A SyntaxError comes from the parser. The file is not grammatical, so nothing runs, not even line 1. Here is David's program with its mistake:

console.log("Starting the report...");
const shop = "Bob's Books";
const sold = 12;
const price = 250;
const total = sold * price;
console.log(shop);
console.log("Books sold: " + sold);
console.log("Price each: " + price);
console.log("Total: " + total);
console.log("Done." +);

The Playground marks the run as a Compile error and prints this in its Compile output tab:

/box/main.js:10
console.log("Done." +);
                     ^

SyntaxError: Unexpected token ')'

The Playground checks your file with the parser before it runs it, which is why it says "compile". The + promised a second value and found ) instead. /box/main.js is the name your code gets inside the Playground's box, and :10 is the line. Node adds two internal lines and its version number under the message; they are left out here.

A ReferenceError comes while the code runs. The grammar was fine, so the program started. Then it reached a name that does not exist. Everything before that line has already run, as Example 2 shows.

So a SyntaxError means "nothing ran", and a ReferenceError means "it ran until this line".

Example 1: the function we followed

The line from Figure 1, inside a small function, called once.

function total(price, count) {
  return price * count;
}

console.log(total(250, 3));
750

Called once, total never gets hot, so it stays bytecode. That is fine: three instructions run in well under a millionth of a second.

Run in Compiler
Example 2: a ReferenceError, after a line that ran

Line 3 has a typo: totl instead of total.

console.log("Line 1 runs.");
const total = 5;
console.log(totl);
console.log("Line 4 never runs.");
Line 1 runs.

After that line, the Playground shows a Runtime error with this message:

/box/main.js:3
console.log(totl);
            ^

ReferenceError: totl is not defined

Line 1 printed, because the parser was happy and the program started. Line 3 failed while running. Line 4 never ran. Node also prints a list of internal lines under the message, called a stack trace; Module 10 teaches you to read it.

Run in Compiler
Example 3: watching the JIT warm up

This program calls a tiny function 100,000 times, five rounds in a row, and times each round. You do not need to read every line yet; the loop is Module 4's subject.

function add(a, b) { return a + b; }
for (let round = 1; round <= 5; round++) {
  const start = process.hrtime.bigint();
  let sum = 0;
  for (let i = 0; i < 100000; i++) sum = add(sum, i);
  const micros = Number(process.hrtime.bigint() - start) / 1000;
  console.log("Round " + round + ": " + micros.toFixed(0) + " microseconds");
}
Round 1: 5415 microseconds
Round 2: 4376 microseconds
Round 3: 4169 microseconds
Round 4: 492 microseconds
Round 5: 147 microseconds

One run on the Playground (Node.js 22), on 7 October 2026. The same work got about 37 times faster between round 1 and round 5, because the engine compiled the hot code to machine code. Your numbers will differ on every run, but the drop will be there.

Run in Compiler

Where this is used

  • Chrome, Edge and Node.js all run V8, so the bytecode in this lesson is the same in a browser tab and on a server.
  • Firefox runs SpiderMonkey, which has its own interpreter and its own JIT tiers under different names, following the same rulebook.
  • Safari and the Bun runtime run JavaScriptCore, Apple's engine, which also starts in an interpreter and compiles hot code.
  • The Progsity Playground runs Node.js 22, which carries V8 12.4. It parses your file first and shows a SyntaxError as a Compile error before anything runs.

Common mistakes

1. Hunting for the bug among the lines that "ran".

David looked at line 1 and wondered why it did not print. After a SyntaxError, no line ran. Go straight to the line the message names, and read the token it points at.

2. A capital letter that makes a new name.

const total = 750;
console.log(Total);

Node.js 22 says ReferenceError: Total is not defined. JavaScript treats Total and total as two different names, and only one exists. Copy names exactly, capitals included. You will do this most often after renaming something in one place but not another.

3. A missing bracket, reported one token later.

function total(price, count {
  return price * count;
}

The Playground shows a Compile error with SyntaxError: Unexpected token '{'. The parser only noticed the missing ) when it met the {. When a message points at a token that looks fine, check what should have come just before it.

4. Believing JavaScript is slow because it is "interpreted".

Example 3 shows hot code running about 37 times faster once it is compiled. For most programs the engine is fast enough that your algorithm matters far more than the language. Module 17 measures this properly.

Brain teaser

Two programs. Each starts with a line that prints Line 1, and each has a mistake on line 9, inside a function that is never called. One has a SyntaxError, the other a ReferenceError. What does each program print?

console.log("Line 1");

function neverCalled() {
  console.log("inside");
}

function alsoNeverCalled() {
  const x = 1;
  return x +;
}
console.log("Line 1");

function neverCalled() {
  console.log("inside");
}

function alsoNeverCalled() {
  const x = 1;
  return y + x;
}

Ask which stage each mistake belongs to. Does that stage look at every line of the file, or only at the lines that actually run?

Exercise 1Easy

Open Example 1 and change the call to total(120, 4). Run it.

Check yourself. It prints 480. Then change * to + and run again: it prints 124.

Exercise 2Medium

Break Example 1 on purpose, twice, and record what the Playground says each time. Repair it between the two.

The two breaks. First, delete the ) after count on line 1. Second, change count on line 2 to cont.

Rules. For each break, write the error class, the line number it names, and whether the Playground says Compile error or Runtime error.

Check yourself. One break is a SyntaxError and nothing prints. The other is a ReferenceError that names line 2. If you got two errors of the same class, reread "Two errors, two stages".

Exercise 3Hard

See bytecode yourself. This one needs Node.js installed on your own machine, which lesson 05 shows how to do. Save Example 1 as total.js and run this in a terminal:

node --print-bytecode --print-bytecode-filter=total total.js

Rules. Find the three instructions from this lesson in the printout. Then change * to + in the file, run the command again, and say which instruction changed.

Check yourself. Exactly one instruction changes, and its new name says what it does. The Playground cannot run this command, because it does not take flags.

Common doubts

  • Do I need to understand bytecode to write JavaScript?

    No. You will never write it. Knowing it exists explains two everyday facts: why errors come in classes, and why code speeds up as it runs.

  • Why does the Playground say "Compile error" if JavaScript is not compiled?

    Before running your file, the Playground asks Node to parse it, the way stage 1 does. If the parser refuses, nothing runs, so the result is shown as a compile error. It is the parser speaking, not a C-style compiler.

  • Which engine is the fastest?

    It changes from year to year as each team improves its own. All three are fast enough that it almost never decides how fast your page or server feels.

  • Does the JIT make every program fast?

    Only the parts that run many times. A short program finishes while still in the interpreter, which is fine, because it was already quick.

  • Can the JIT's guesses give a wrong answer?

    No. When a guess turns out wrong, the engine drops the machine code and goes back to bytecode. The answer stays correct; only the speed changes for a while.

Key takeaways

  • Three engines run the JavaScript in nearly every browser and server: V8 (Chrome, Node.js), SpiderMonkey (Firefox), JavaScriptCore (Safari).
  • The parser reads the whole file into a parse tree before any line runs.
  • Ignition turns the tree into bytecode and runs it; TurboFan compiles hot code to machine code, just in time.
  • "Interpreted" and "compiled" are both half right: JavaScript is interpreted first and compiled where it pays.
  • A SyntaxError comes from the parser and nothing runs; a ReferenceError comes during the run, after earlier lines ran.

Next you run your own first line, in three places, and get the same answer from all three.

End of lesson 4

Mark it done, and your progress moves with you.

Next: Your First Run: The Console, Node, and the Playground