How To Link JavaScript To An HTML File Without Breaking It

Here is how to link JavaScript to an HTML file in one line, where the script tag actually goes, and why a script in the head quietly does nothing at all.
The OxyPages Team · · 9 min read
You have two files in one folder. index.html, and script.js right next to it. You open the page and nothing happens. No alert, no click handler, no message in the console. The JavaScript may as well not exist.
As far as the page is concerned, it doesn't. An HTML file has no idea another file is beside it until you tell it, and you tell it with one tag:
<script src="script.js" defer></script>
Put that in the <head> and you are done. The defer is what makes it safe there. That one line is how you link JavaScript to an HTML file, and for plenty of people it is the whole answer.
But this gets asked constantly for a reason. The tag is easy. The tag in the wrong place is what eats an afternoon, because it fails quietly: no error you can see, just a button that does nothing.
Often an AI wrote both files and handed them over as a pair. That changes nothing here, and what to do with a page an AI just handed you covers the rest of that situation.
TL;DR: One tag connects the two files: <script src="script.js"></script>. Put it in the <head> with defer on it, or immediately before the closing </body> tag. A plain script in the head runs before the page exists, so document.getElementById returns null and nothing works. Never self-close the tag: <script src="app.js" /> silently swallows the rest of your page. When it still fails, the browser console names the problem in seconds.
How Do You Link JavaScript To An HTML File?
With one <script> tag pointing at the file. Everything else here is about where that tag goes and why it sometimes does nothing.
Here is the smallest working pair. Two files, same folder.
<!DOCTYPE html>
<html lang="en">
<head>
<title>My Page</title>
</head>
<body>
<h1>Hello</h1>
<script src="script.js"></script>
</body>
</html>
console.log("The script is running.");
Open index.html, press F12, and look at the Console tab. If the message is there, the two files are connected and the rest is detail.
Two rules about the tag itself:
srcis a path, not a name. It is resolved relative to the HTML file, exactly like an image or a stylesheet.- The closing
</script>is not optional. More on that below.
Filenames are yours to pick. script.js, app.js and main.js are conventions, not rules.
Where The Script Tag Goes Is The Whole Ballgame
Almost every "my JavaScript isn't working" question is this one problem wearing a different hat.
Why A Script In The Head Finds Nothing
A browser reads an HTML file from top to bottom. When it reaches a plain <script src="..."> tag it stops reading, downloads the file, runs the whole thing, and only then carries on with the rest of the page.
Put that tag in the <head> and the stop happens before the <body> has been read. Your buttons, your headings, the paragraph you want to change: none of them exist yet.
So this fails.
<head>
<script src="script.js"></script>
</head>
<body>
<button id="go">Click me</button>
</body>
const button = document.getElementById("go");
button.addEventListener("click", () => alert("Hi"));
And the console shows this.
Uncaught TypeError: Cannot read properties of null (reading 'addEventListener')
getElementById returned null because at the moment that line ran there was no button on the page to find. The code is correct. The timing is not. If you pasted that error into a search box today, this is your bug.
The Three Fixes, And When Each One Is Right
Move the tag to the bottom. Put <script src="script.js"></script> immediately before the closing </body> tag. By the time the browser gets there the whole page has been read, so everything your script reaches for exists. This always works and requires you to remember nothing.
Add defer and leave the tag in the head. Writing <script src="script.js" defer></script> tells the browser to download the file in the background while it keeps reading the page, then run it once the page is fully parsed. Same result as the bottom, except the download starts earlier, so the page is ready sooner.
Add async only when the script touches nothing on the page. <script src="analytics.js" async></script> downloads in parallel and runs the instant it arrives, which may be before or after the page is ready. Correct for a tracking script, wrong for anything that reads or changes your content.
If you want one rule to follow: use defer. It keeps your scripts in the head where they are easy to find, it starts the download early, and it preserves the order of multiple scripts. Move the tag to the bottom instead when you are editing a page you did not write and the head is somebody else's territory.
| Where the tag is | When the script runs | Order kept | Use it when |
|---|---|---|---|
| Head, plain | Straight away, before the page exists | Yes | Almost never |
Just before </body> | After the whole page is read | Yes | You cannot safely touch the head |
Head, with defer | After the page is parsed, before DOMContentLoaded | Yes | Your default choice |
Head, with async | The instant the download finishes | No | Analytics and scripts that ignore the page |
| Head, as a module | Deferred automatically | Yes | You are using import |
Never Self-Close The Script Tag

<script src="app.js" /> does not work in HTML. It is not a shorter way to write the tag. It is a broken tag.
HTML has a fixed list of void elements, like <img> and <br>, which never take a closing tag. The script element is not one of them. The browser ignores the slash, keeps the element open, and swallows the rest of your page as script contents until it meets the next </script>.
Usually there is one further down, the element closes there, and your src file does get fetched and run. So the usual symptom is a page cut in half by a script that did run. If nothing below ever closes it, the file never runs.
People arrive at this from React and JSX, where self-closing tags are normal. The habit does not survive plain HTML.
Write both halves every time, even when there is nothing between them.
<script src="app.js"></script>
Stylesheets are different: <link href="style.css" rel="stylesheet"> is on that void list, so it has no closing half. MDN on the script element
Getting The File Path Right
The src value is a path, and it follows the same rules as any other file the page points at.
script.jsmeans "in the same folder as this HTML file", which is what you want for a two-file project.js/script.jsmeans "inside the js folder, which sits next to this HTML file".../script.jsmeans "one folder up from here"./script.jsmeans "at the root of the site", which on a real host is the top of your domain. Opened from your desktop it points at the root of your hard drive instead, which is why a leading slash can work online and fail locally.
Two more that catch people out.
Capital letters matter everywhere except your own laptop. Windows and macOS treat Script.js and script.js as the same file. Linux, which is what most web servers run, does not. A page that worked on your desktop can arrive online with a dead script because of one capital letter.
Move the folder, not the file. If your page points at script.js, a stylesheet and an images folder, all of those are part of the page. Uploading the whole folder rather than one file is the difference between a working page and a dead one.
How To Check It Actually Loaded

Press F12 to open developer tools and work through this in order. It takes about twenty seconds and tells you which failure you have.
- Console tab, look for red.
Uncaught SyntaxErrormeans the file loaded and your JavaScript has a typo.Cannot read properties of nullmeans it loaded but ran too early. - Network tab, then reload the page. Find your
.jsfile in the list. Status 200 means the browser fetched it. Status 404 means the path or the filename is wrong. - No entry in the Network list at all? The browser never asked for the file. That is a typo inside the tag, or a self-closed tag that ate it.
- Add a
console.logline at the top of your script. If it appears, the file is running and the problem is further down in your own code.
That order matters. Most people start by rewriting their JavaScript when the browser never fetched the file in the first place.
A Complete Two-File Example You Can Copy
Two files in one folder. Clicking the button visibly changes the page, so you can tell at a glance whether the wiring worked.
Save this as index.html.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Counter</title>
<script src="script.js" defer></script>
</head>
<body style="font-family: system-ui; padding: 40px;">
<h1 id="count">0</h1>
<button id="add">Add one</button>
</body>
</html>
Save this next to it as script.js.
let count = 0;
const heading = document.getElementById("count");
const button = document.getElementById("add");
button.addEventListener("click", function () {
count = count + 1;
heading.textContent = count;
heading.style.color = count > 4 ? "green" : "black";
});
Open index.html and click. The number climbs and turns green after five clicks. If it does, defer is doing its job: the script sits in the head and still finds the button.
What Changes When The Script Is A Module
Adding type="module" to the tag, as in <script src="app.js" type="module"></script>, turns the file into an ES module. Three things change. It is deferred automatically, so placement stops mattering. You can use import to pull in code from other files. And it will not run from a file:// address at all.
That last one surprises everybody. Double-click index.html on your desktop and a module script fails before it starts, with a console message about CORS and the file origin. The same folder, uploaded to any host, works immediately.
Modules are fetched over the network like any other resource, and a file opened off your disk has no origin for the browser to check. There is no setting to turn it off. MDN on JavaScript modules
The same goes for fetch and for service workers. A lot of ordinary JavaScript needs a real origin.
Two ways out. Run a local server, which means a terminal and a command like npx serve. Or put the folder on a real host, which takes about a minute and gives you a shareable link. Netlify Drop takes a dragged folder without an account, and Cloudflare Pages is free once you sign up.
Being straight about the limit: a static host serves your files and nothing else. It will not run Node, install npm packages, or give your script a backend to call. If your JavaScript needs a server of its own, hosting the folder does not solve it, and OxyPages is the wrong tool.
What To Do Next
Five minutes, in this order.
- Put the
.jsfile in the same folder as your HTML file. - Add
<script src="script.js" defer></script>inside the<head>. - Check both halves of the tag are there and nothing is self-closed.
- Open the page, press F12, and read the Console tab, then the Network tab.
- Once it works locally, put the whole folder online.
That last step stops being optional the moment you use a module, fetch, or anything that loads a second file at runtime. Those need an address, not a file path.
Drop the whole folder into OxyPages and you get a working HTTPS link in under a minute, which is also the quickest way to test a module script. Any free static hosting does the same job.
FAQ
Should The Script Tag Go In The Head Or The Body?
Either works if you use defer. Without it, put the tag immediately before the closing </body> tag, because a plain script in the head runs before the elements it wants to reach exist. defer in the head is the better habit: the download starts sooner.
Why Is My JavaScript File Not Loading?
Open the Network tab and reload. A 404 means the path in src is wrong. No entry at all means the tag is malformed, usually a self-closed script tag. A 200 with nothing happening means the file loaded, and the Console will say whether it is a timing problem or a typo.
Can I Add More Than One JavaScript File To A Page?
Yes, as many as you want. Use one <script> tag per file. Plain and defer tags run in the order you wrote them, so a file that depends on another goes second. async tags ignore order, which is why they are only safe for scripts that stand alone.
Do I Need A Server To Link The JavaScript To The HTML?
Not for a normal script. Double-clicking index.html is enough to link the JavaScript to the HTML and watch it run. You do need one for type="module", for fetch, and for anything that reads another file at runtime, because those require a real origin. A local server works, and so does uploading the folder, which gives you a shareable link at the same time.