Q1In a script loaded in head without defer, what does document.querySelector("#total") return?
How JavaScript Runs in the Browser
How the browser builds the DOM from HTML, the order in which it processes link and script tags, the defer attribute, the DevTools Console, and checking results as text in the practice console.
Code that changes text on the page with JavaScript can work or fail depending on where you load it. When it fails, no error appears on the page, so you can't tell whether the code itself is wrong or the problem is the order things run in.
This article covers the order in which the browser loads HTML and runs JavaScript.
The Code in This Article Is Meant to Be Opened as HTML
The practice console can't reproduce the order in which HTML is loaded, so this article has no exercises. Each example shows what happens when you put index.html and app.js in the same folder and open index.html in Chrome, with the results written as comments at the end of the lines. How to check things in the console is covered in the last section.
Building Nested Elements from HTML — the DOM Tree
Say you want app.js to change the "Calculating…" text on an order confirmation page to "3,300 yen". What JavaScript changes here isn't the text in the index.html file. It works on separate data that the browser built by reading the HTML.
The browser turns each HTML element (a piece of the page marked off by tags, like <p>…</p>) into an object, and the whole set of their parent-child relationships is called the DOM (Document Object Model, also called the DOM tree; its entry point is document).
Settings written inside a tag, like id="total" and defer, are called attributes. An id must be unique: no other element on the page can use the same one.
<!-- ---- index.html (order confirmation page) ---- -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Order Confirmation</title>
<link rel="stylesheet" href="style.css"> <!-- CSS that controls the look -->
</head>
<body>
<h1>Order Confirmation</h1>
<ul id="items"> <!-- has two li children -->
<li>Mug × 2</li>
<li>Coaster × 1</li>
</ul>
<p id="total">Calculating…</p> <!-- the element app.js wants to change -->
</body>
</html>
meta— character encoding is UTF-8title— contains "Order Confirmation"link— points to style.css
- Contains "Order Confirmation"
li— contains "Mug × 2"li— contains "Coaster × 1"
- Contains "Calculating…"
- The element app.js wants to change
app.js gets the p element with document.querySelector("#total") (which finds the element with the id written after the #, or returns null if there isn't one) and changes its textContent (the text inside the element). Since the p element is a child of body, whether it's found depends on whether body is in the DOM yet.
Reading HTML from the Top — the Order of link and script
Suppose you load app.js, which updates the total, in <head> next to the CSS. index.html clearly contains <p id="total">, yet when you open the page it still says "Calculating…", and the update in app.js has no effect.
During HTML parsing (the browser reading the HTML from the start and adding each element it finds to the DOM in order), when the browser reaches a <link>, it keeps parsing while it fetches the CSS. When it reaches a <script src>, it pauses parsing, fetches the JavaScript, and runs it to the end before reading the rest.
// ---- index.html (loading app.js in head) ----
// <head>
// <link rel="stylesheet" href="style.css"> ← parsing continues
// <script src="app.js"></script> ← parsing pauses to run it
// </head>
// <body>
// <p id="total">Calculating…</p> ← added to the DOM after app.js
// </body>
// ---- app.js ----
const total = document.querySelector("#total"); // null (not in the DOM yet)
total.textContent = "3,300 yen";
// TypeError: Cannot set properties of null (setting 'textContent')
The update didn't work because the p element wasn't in the DOM yet when app.js ran. In the next section, you'll leave the two lines of app.js as they are and move the point at which they run to after the DOM is complete.
Running After the DOM Is Complete — the defer Attribute
If you move <script> to just before </body>, it runs after the p element has been read, so #total is found. But then whether it works depends on where the tag sits, and if you later move the loading line back into <head> with the CSS, you get the same null again.
The defer attribute (a <script> setting that fetches JavaScript without pausing HTML parsing and runs it once the DOM is complete) is written as <script defer src="app.js"></script>, and it can go in <head> just like the CSS <link>.
// ---- index.html (loading in head with defer) ----
// <head>
// <link rel="stylesheet" href="style.css">
// <script defer src="app.js"></script> ← starts fetching, parsing continues
// </head>
// <body>
// <p id="total">Calculating…</p> ← app.js runs after everything up to </html> is read
// </body>
// ---- app.js (the same 2 lines as before, plus 1 to check) ----
const total = document.querySelector("#total"); // the p element
total.textContent = "3,300 yen"; // the text on the page changes
console.log(total.textContent); // 3,300 yen
When there are several deferred scripts, they run in the order they appear in the HTML, not the order their fetches finish. The table below shows, for each way of writing the script, when app.js runs and whether #total is found.
| How the script is written | When app.js runs | #total |
|---|---|---|
| <script src="app.js"> in head | Right away, pausing parsing | Not found (null) |
| <script src="app.js"> before </body> | After the p is read | Found |
| <script defer src="app.js"> in head | After the DOM is complete | Found |
| Inline code in <script defer> in head | Right away (defer is ignored without src) | Not found (null) |
| <script type="module" src="app.js"> in head | After the DOM, even without defer | Found (not when opened as a file) |
Finding Where It Stopped and Inspecting the DOM — DevTools
On the page before defer was added, app.js threw a TypeError, but nothing showed up on the page. Without knowing which file and line it failed on, you can't track down why the page is stuck on "Calculating…".
DevTools (a panel built into the browser for inspecting a page's DOM and errors) has a Console tab, which in Chrome opens with Ctrl + Shift + J on Windows or Command + Option + J on Mac. It lists console.log output, along with any errors and the file name and line number where each was thrown.
// ---- Error shown in the Console tab (click app.js:2 on the right to open that line) ----
// Uncaught TypeError: Cannot set properties of null (setting 'textContent')
// at app.js:2:19 ← thrown at line 2, column 19
// ---- After the page loads, type this after the > in the Console and press Enter ----
document.querySelector("#total"); // <p id="total">Calculating…</p>
document.querySelector("#total").textContent; // 'Calculating…'
// ---- A line to add after line 1 of app.js (don't type this in the Console) ----
console.log("total at run time:", total); // total at run time: null
An expression you type in the Console runs against the DOM as it is at that moment. To check a value inside app.js, add a console.log before the line you suspect, so that the value at the time the code runs is printed in the Console tab.
Checking Results as Text — innerHTML and outerHTML
In the exercises in the articles that follow, you'll find, change, and add elements. But the practice console has no visible page, and until you reload, every exercise shares a single invisible page, so each run starts with the elements from the previous run still in place.
Assigning to innerHTML (a property that reads and writes an element's contents as an HTML string) replaces the contents entirely. Each exercise starts by assigning to the innerHTML of document.body (the body element) to clear out earlier elements, and you check the result by printing outerHTML (an HTML string that includes the element's own tags).
// ① Replace everything inside body (elements left over from the last run are removed here)
document.body.innerHTML = `<p id="total">Calculating…</p>`;
// ② Find the element and change its text
const total = document.querySelector("#total");
total.textContent = "3,300 yen";
// ③ The page isn't visible, so print the results as strings
console.log(total.textContent); // 3,300 yen
console.log(total.outerHTML); // <p id="total">3,300 yen</p>
console.log(document.body.innerHTML); // <p id="total">3,300 yen</p>
- Only
console.logoutput comes back, as text
- Contents of
document.body— kept until the next run
const total— usable only within this run- Assigns a p to
document.body.innerHTML
const total— created as a new variable, separate from last time- The assignment at the start removes the first run's p
Code in the console runs inside a new function each time, so const total is created as a separate variable from the previous run, and you don't get a redeclaration error. It also runs after the page has finished loading, so you don't need to think about defer.
Printing an Element Directly Shows {}
The practice console has no page to show and converts output to text before returning it. If you pass an element straight to console.log, it shows up as {} with nothing visible inside, so you can't tell whether your change worked. To check an element, print its outerHTML or textContent.
Knowledge Check
Answer each question one by one.
Q2When does app.js run with <script defer src="app.js">?
Q3If you run document.body.innerHTML = '<p>Calculating…</p>'; twice, how many p elements are there?