Module 0 ¡ What JavaScript Is, Where It Runs, and How an Engine Runs It
How the Engine Runs Your Code: Parse, Bytecode, JIT
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.
| Engine | Made by | Where you meet it |
|---|---|---|
| V8 | Chrome, Edge, Node.js, Deno, and the Playground | |
| SpiderMonkey | Mozilla | Firefox |
| JavaScriptCore | Apple | Safari, 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.
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.
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.
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".
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.
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 CompilerThis 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 CompilerWhere 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.
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.
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".
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