Skip to content

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.

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:

BrowserJavaScript Engine
Chrome, EdgeV8
FirefoxSpiderMonkey
SafariJavaScriptCore

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.

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.

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:

  1. greet('Brandon') is called — pushed onto the stack
  2. Inside greet: console.log('Hello, Brandon') is called — pushed and immediately resolved
  3. greet finishes — popped off the stack
  4. console.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.

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.

The most common beginner error is code that runs before the HTML it needs has been parsed:

// This script runs in the <head> without defer
const nav = document.querySelector('nav');
nav.classList.add('loaded'); // TypeError: Cannot set properties of null

document.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:

  1. Browser requests index.html from the server
  2. Server responds with the HTML
  3. Browser begins parsing the HTML top to bottom
  4. Browser finds <link rel="stylesheet" href="global.css"> — starts downloading CSS in the background
  5. Browser finds <script src="main.js" defer> — starts downloading JS in the background, continues parsing HTML
  6. HTML parsing completes
  7. CSS finishes downloading — browser applies styles and renders the page
  8. main.js finishes downloading — engine parses and compiles it
  9. 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.

Deliberately recreate the timing error so you can recognize it:

  1. In your base layout, temporarily remove defer from the script tag and move it to the top of <head>:
    <script src="/scripts/main.js"></script>
  2. In main.js, add this line:
    const nav = document.querySelector('nav');
    console.log(nav);
  3. Reload the STO page in Chrome and open the Console.
  4. You will see null logged — the nav element was not in the DOM yet when the script ran.
  5. Restore defer and reload. Now nav logs 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 SyntaxError during 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 return null and crash. defer solves this by waiting for full HTML parsing.