Your First JavaScript Script: The script Tag, defer, async and DevTools
We link JavaScript to HTML inline and as an external file, see how defer and async differ, and try console methods, reading error messages and our first breakpoint in DevTools.
Version note: The code in this post targets ES2026 and current browsers (Chrome, Edge, Firefox, Safari).
Predict First
This time we start with a small HTML file instead of the console. Save the code below on your computer as predict.html, but hold on a minute before you open it.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Predict</title>
<script>
console.log(document.body);
</script>
</head>
<body>
<h1>Hello</h1>
</body>
</html>
When you open the file in the browser and look at the console, what will you see? The body element itself, an error, or something else entirely? Make a guess before you keep reading. We'll reveal the answer once we've talked about how the browser reads an HTML file.
The script Tag: JavaScript's Way Into the Page
The console we saw earlier is perfect for quick experiments, but everything you type there vanishes the moment you reload the page. To make your code stick, it has to become part of the page. That's what the <script> tag is for.
In its simplest form, you write the code right inside the tag. This is called an inline script:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>First Script</title>
</head>
<body>
<h1>Hello</h1>
<script>
console.log("Page loaded, the script ran.");
</script>
</body>
</html>
Save it as first.html, double-click the file to open it in your browser, then open the console. If you don't see the message, reload the page once with the console open. No server, no setup; the browser read the file from disk and ran it.
External Files: Giving Your Code Its Own Home
An inline script is fine for a few lines, but as the code grows it starts to get lost inside the HTML. The far more common approach is to put the JavaScript in a separate .js file and link it with the src attribute. Create two files in the same folder. First index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>External Script</title>
</head>
<body>
<h1>Hello</h1>
<script src="app.js"></script>
</body>
</html>
Then app.js next to it:
console.log("Greetings from app.js!");
A few things to watch out for here. If you write code inside a <script> tag that has a src, that code is ignored; it's either the file or the content, never both. You always need to close the tag with </script>. A self-closing form like <script src="app.js" /> doesn't work in HTML: the browser still treats the tag as open and takes the HTML that follows as the script's content. And the JS file itself never contains a <script> tag; it's pure JavaScript.
A separate file isn't just about tidiness: the browser can cache it, and you can reuse the same file across several pages.
Where Should the Script Go?
Now back to the opening question. The answer: you'll see null in the console.
To understand why, you need to know how the browser reads HTML. It reads the file from top to bottom and builds the structure of the page as it goes. When it reaches a <script> tag, it stops, runs the script, and only then carries on from where it left off. If the script is an external file, it also waits for that file to download first.
In our prediction file, the script sits inside <head>. When the browser reaches that line, it hasn't read the <body> tag yet; there is no body. That's why document.body returns null, which is its way of saying "nothing here yet." Move the same script to just before the closing </body> and you'll see the body element in the console, because by that point the body has already been built.
Think of a builder working from a blueprint. While putting up the walls, the builder drops everything at each script line to carry out the instruction. If the instruction says "paint the kitchen cabinets" and the kitchen hasn't been built yet, there's nothing to paint. And if the instruction arrives late, the builder just stands there waiting while the user stares at a blank screen.
For many years, the fix was to put every script just before </body>. That still works. But today we have a better tool.
defer and async: Download It, but Don't Make Me Wait
You can add two attributes to external scripts: defer and async. Both start with the same promise: "Download this file in the background and keep reading the HTML." The difference is what happens once the download finishes.
<head>
<meta charset="UTF-8">
<title>defer and async</title>
<script src="app.js" defer></script>
<script src="counter.js" async></script>
</head>
defer says: "Download it, but don't run it until the whole page has been read." If there are several deferred scripts, they run after the page is parsed, in the order they appear in the HTML. If you move the line from the prediction file into an external file and add defer, you'll see the body instead of null, even though the tag is still inside <head>.
async says: "Download it and run it the moment it arrives." As soon as the download is done, the browser briefly pauses reading the HTML and runs the script. Since you can't know which file will arrive first, the order of several async scripts isn't guaranteed, and the page may not be fully parsed when they run.
Think of it as waiting for deliveries. A normal script is like reading a book while waiting at the door for a parcel; you can't turn a single page until it shows up. defer is placing the order, going back to your book, and opening the parcels in the order you ordered them once you've finished reading. async is also placing the order and reading on, but dropping the book to open whichever parcel arrives, the moment it arrives.
The practical rule: for scripts that work with the page and depend on each other, use defer and put them in <head>. async fits standalone things that touch neither the page nor other scripts, like a visit counter. When in doubt, pick defer.
Two fine points. These attributes are meant for external scripts linked with src; if you put defer on an inline script, the browser ignores it. Also, scripts loaded with type="module" behave as if deferred by default; we'll cover modules in detail in upcoming posts.
The Console Is More Than log
The console.log we saw earlier has a few siblings, and each gives your message a different meaning. Paste these into your browser's console and try them:
console.log("Plain info", 42, true);
console.warn("Heads up: stock is running low");
console.error("Payment failed");
console.log prints every comma-separated value side by side. console.warn shows up as a yellow warning, console.error as a red error. With the level filter at the top of the console you can show only warnings or only errors; if you log at the right level, you'll find what you're looking for quickly even in a crowded console.
There's also console.table, which shows several records as a table:
const products = [
{ name: "Pen", price: 15 },
{ name: "Notebook", price: 40 },
{ name: "Eraser", price: 8 }
];
console.table(products);
Here we built an array with square brackets and objects with curly braces; we'll cover both in detail in upcoming posts. We'll see const in the next post; for now read it as "I'm giving this value a name." The console shows a neat table where each product is a row and name and price are columns.
Reading an Error Message
An error message looks scary at first, but it's really a note telling you what broke and where. Let's reuse the index.html from before. Replace the contents of app.js with this and reload the page:
console.log("Started");
consol.log("This line will throw an error");
console.log("We never get here");
The console first prints "Started," then a red line appears: Uncaught ReferenceError: consol is not defined. Read it piece by piece. Uncaught means the error wasn't caught anywhere; we'll cover catching errors in upcoming posts. ReferenceError is the type of error: the engine couldn't find something you referred to by name. consol is not defined is the explanation itself. The app.js:2 link on the right of that line tells you which file and line the error is in; click it and you jump straight to that line.
Notice one more thing: the third line never ran. An uncaught error stops that script right at that point. Other <script> tags on the page aren't affected, though; they still run when their turn comes.
At the beginning you'll mostly meet three types. ReferenceError: you used a name that isn't defined, usually a typo. TypeError: you tried to do something with a value that it doesn't support; for example, if you write "hello".toUppercase(), you get an is not a function error, because the correct name is toUpperCase. SyntaxError: the code breaks the rules of the language, like an unclosed parenthesis or quote.
That last one behaves differently from the others. The engine reads the whole file before running it; if it finds something that breaks the rules, it doesn't run a single line. So if a parenthesis is missing on the last line of the file, even the console.log on the first line won't run.
The wording of the message can vary a little between browsers, but the type and the line number are always there. When you see an error, the first move is: read the type, read the message, click the line.
Your First Breakpoint: Freezing the Code to Look Inside
Printing values with console.log is the fastest way to debug, but sometimes you want to freeze the code on an exact line and inspect what's going on at that moment. That's what a breakpoint does. Change app.js to this:
const price = 120;
const discountRate = 0.25;
const discount = price * discountRate;
const total = price - discount;
console.log("Discounted price:", total);
In the developer tools, switch to the Sources tab in Chrome and Edge, or the Debugger tab in Firefox. Open app.js from the file list and click the number of line 4; the line gets marked. Now reload the page.
The code stops at line 4 and the page goes into a paused state. That line hasn't run yet, because a breakpoint pauses right before the line executes. Hover over the word discount and you'll see the value 30. The Scope panel next to it lists the variables you can reach at that moment. In Chrome, Edge and Firefox, F10 moves one line forward and F8 resumes the code from where it stopped.
You can do the same thing from inside the code. Just write debugger; where you want to stop:
const price = 120;
const discountRate = 0.25;
const discount = price * discountRate;
debugger;
const total = price - discount;
console.log("Discounted price:", total);
With the developer tools open, the browser stops on this line; with them closed, it walks right past it. Remember to delete your debugger lines when you're done, or one day you'll open the panel and your page will freeze somewhere you didn't expect.
A Quick Note on "use strict"
In older code you may see an odd string sitting alone at the very top of a file:
"use strict";
counter = 1;
console.log(counter);
This line turns on strict mode, which arrived with ES5 in 2009. Strict mode turns some mistakes that JavaScript used to let slide silently into real errors. In the example, counter is used without ever being declared. In normal mode the browser says nothing and quietly creates a global variable behind the scenes; in strict mode you get ReferenceError: counter is not defined. The second outcome is much better, because your typo shows up right away.
For the directive to work, it has to be the first statement of the file or function. The good news: modules loaded with type="module" run in strict mode automatically, so you'll see this line less and less in modern code. We'll write the examples in this series so that they also run fine in strict mode.
A Small Experiment
★☆☆ Create an index.html and an app.js, and link the script from <head> with defer. In app.js, print three messages with console.log, console.warn and console.error, then use the console's filter to show only the errors.
★★☆ Predict the output: <head> contains <script src="one.js" defer></script> and <script src="two.js" defer></script>, and right before </body> there's an inline script with console.log("inline"). one.js prints "one" and two.js prints "two". What order will you see in the console? Then replace both defer attributes with async and reload a few times; what can you guarantee about the order, and what can't you?
★★★ Put console.log("first line") on the first line of app.js, and on the last line write console.log("last line" with the closing parenthesis missing. Will "first line" be printed? Predict, try it, and read the error type and line number in the console. After fixing the error, set a breakpoint on the second line and step through to the end of the file with F10.
In this post we brushed off const as "giving a value a name." In the next post we'll focus on variables themselves: the difference between let and const, block scope, naming rules, and why var has been put out to pasture.