How the Browser Runs JavaScript
You have written JavaScript, linked it to a page, and watched it run. Now it is worth understanding what the browser is actually doing when it executes your code. This is not optional background knowledge — it directly explains errors you will hit throughout this course.
Every browser has a JavaScript engine
Section titled “Every browser has a JavaScript engine”When a browser loads a page containing JavaScript, a component called the JavaScript engine reads and executes the code. Every major browser ships with its own engine:
| Browser | JavaScript Engine |
|---|---|
| Chrome, Edge | V8 |
| Firefox | SpiderMonkey |
| Safari | JavaScriptCore |
The engines are different implementations of the same language specification (ECMAScript). Code that runs in one engine runs in all of them — with rare exceptions involving very new features.
What the engine does with your file
Section titled “What the engine does with your file”When the engine receives a .js file, it works through three stages:
1. Parsing — The engine reads the source code character by character and checks it for valid syntax. If it finds a typo — a missing closing bracket, an unexpected symbol — it throws a SyntaxError immediately and stops. None of the code runs.
2. Compilation — The engine translates the valid code into a lower-level representation it can execute efficiently. Modern engines like V8 do this with a just-in-time (JIT) compiler.
3. Execution — The engine runs the code, one statement at a time, in the order they appear.
This is why a single SyntaxError anywhere in a file prevents the entire file from running. The browser refuses to execute code it cannot parse.
The call stack
Section titled “The call stack”JavaScript is single-threaded, which means it can only do one thing at a time. The mechanism that tracks what the engine is currently doing is called the call stack.
Think of the call stack like a stack of plates:
- When a function is called, a new plate goes on top of the stack
- When the function finishes, that plate is removed
- The engine always works on the top plate
function greet(name) { console.log('Hello, ' + name);}
greet('Brandon');console.log('Done');Execution order:
greet('Brandon')is called — pushed onto the stack- Inside
greet:console.log('Hello, Brandon')is called — pushed and immediately resolved greetfinishes — popped off the stackconsole.log('Done')executes
The call stack is what makes stack traces in error messages readable — each line in the trace is one level of the stack at the moment the error occurred.
Synchronous execution
Section titled “Synchronous execution”JavaScript code runs synchronously by default: line by line, top to bottom, one statement fully completing before the next begins.
console.log('Step 1');console.log('Step 2');console.log('Step 3');These always print in order. There is no parallelism. While JavaScript is executing a statement, nothing else — no other script, no browser repaint — can happen.
This is why long-running synchronous operations can freeze the browser. And it is why the defer attribute from the previous lesson is important: the HTML parser and the script loader run in parallel, but the script only executes when parsing is done.
Why timing errors happen
Section titled “Why timing errors happen”The most common beginner error is code that runs before the HTML it needs has been parsed:
// This script runs in the <head> without deferconst nav = document.querySelector('nav');nav.classList.add('loaded'); // TypeError: Cannot set properties of nulldocument.querySelector('nav') returns null because the browser has not parsed the <nav> element yet — it is still below the <script> tag in the HTML. Calling .classList on null crashes.
The fix is defer: the script waits until the entire HTML document is parsed before running, so document.querySelector('nav') finds the element it needs.
Step-by-step: what happens from file request to execution
Section titled “Step-by-step: what happens from file request to execution”Here is the full sequence when a user opens the STO homepage:
- Browser requests
index.htmlfrom the server - Server responds with the HTML
- Browser begins parsing the HTML top to bottom
- Browser finds
<link rel="stylesheet" href="global.css">— starts downloading CSS in the background - Browser finds
<script src="main.js" defer>— starts downloading JS in the background, continues parsing HTML - HTML parsing completes
- CSS finishes downloading — browser applies styles and renders the page
main.jsfinishes downloading — engine parses and compiles it- Engine executes
main.js— your code runs on a fully rendered page
Steps 4, 5, and 7 can overlap. That is why defer produces fast, reliable loading — everything downloads in parallel, and the script runs only when the page is ready.
Exercise
Section titled “Exercise”Deliberately recreate the timing error so you can recognize it:
- In your base layout, temporarily remove
deferfrom the script tag and move it to the top of<head>:<script src="/scripts/main.js"></script> - In
main.js, add this line:const nav = document.querySelector('nav');console.log(nav); - Reload the STO page in Chrome and open the Console.
- You will see
nulllogged — thenavelement was not in the DOM yet when the script ran. - Restore
deferand reload. Nownavlogs the actual<nav>element.
You have just seen the defer difference in action. This is exactly the bug that defer prevents.
- Every browser has a JavaScript engine (V8, SpiderMonkey, JavaScriptCore) that parses, compiles, and executes code.
- A
SyntaxErrorduring parsing stops the entire file from executing. - The call stack tracks what function is currently executing — JavaScript is single-threaded and processes one thing at a time.
- JavaScript executes synchronously: line by line, top to bottom, in order.
- Without
defer, scripts in the<head>run before the page is parsed — DOM queries returnnulland crash.defersolves this by waiting for full HTML parsing.