When JavaScript linked from an HTML page does nothing, the script either never loaded, loaded before the elements it needed existed, or loaded and threw an error you have not seen yet. The browser’s developer console distinguishes the three in seconds. This walks through the checks in the order that finds the cause fastest.
Step 1: open the console#
Press F12, or right-click the page and choose Inspect, then the Console tab. Reload the page. You will see one of four things:
| Console shows | Meaning |
|---|---|
| A red 404 for your script’s filename | The path in the script tag is wrong; the file never loaded |
| Cannot read properties of null | The script ran before the element it wanted existed |
| A SyntaxError or ReferenceError | The script loaded and crashed on that line |
| Nothing at all | The script loaded and ran, and your code did not do what you expected, or the tag is missing |
Add console.log('script loaded') as the first line of the file. If it does not print, the script is not loading or not running. If it prints, the script is fine and the problem is in the code after it.
Cause 1: ran before the HTML existed#
<head>
<script src="app.js"></script> <!-- runs now, before the body is parsed -->
</head>
<body>
<button id="go">Go</button>
</body>
document.getElementById('go').addEventListener('click', run);
// TypeError: Cannot read properties of null (reading 'addEventListener')
The script executed when the parser reached it, and at that moment the button did not exist yet. Two fixes; the first is the modern one:
<script src="app.js" defer></script> <!-- downloads now, runs after the HTML is parsed -->
Or move the script tag to the end of the body, after the elements. Or wrap the code in a DOMContentLoaded listener. defer is the cleanest: the tag stays in the head, and it works for every file.
Cause 2: the path is wrong#
Open the Network tab, reload, and find your script. A 404 in red means the browser asked for a file that is not there. The URL column shows exactly what it asked for; compare it with where the file really is.
<!-- HTML at /index.html, script at /js/app.js -->
<script src="js/app.js" defer></script> <!-- relative to the HTML file's folder -->
<script src="/js/app.js" defer></script> <!-- from the site root; breaks on GitHub Pages subpaths -->
<script src="app.js" defer></script> <!-- wrong: looks in the same folder as the HTML -->
Case matters on most servers: App.js and app.js are different files on Linux hosting even though Windows treats them the same. A page that works locally and 404s when deployed is usually this.
Cause 3: the browser is showing an old copy#
You edited the file, reloaded, and the old behaviour persists. The browser cached the previous version. Hard refresh: Ctrl+Shift+R (Cmd+Shift+R on a Mac). In the developer tools, the Network tab has a Disable cache checkbox that applies while the tools are open; leave it ticked during development.
On a real site, add a version to the filename or query string when deploying, app.js?v=3, so users get the new file.
Cause 4: the script loaded and crashed#
A red error in the console with a line number. Click the link on the right of the error to jump to the line. The most common at this stage are a typo in a variable name (ReferenceError: x is not defined) and a missing bracket (SyntaxError: Unexpected token). One error stops the whole file, so everything after that line also fails to run, which can look like “nothing works” when only one line is wrong.
Cause 5: it is a module#
<script type="module" src="app.js"></script>
Modules are deferred automatically, run in strict mode, and have their own scope — functions defined in a module are not visible to onclick="..." attributes in the HTML. Attach event listeners from inside the module instead. Modules also refuse to load from a file:// URL in most browsers; serve the folder with a local server.
Cause 6: opened from the file system#
If the address bar starts with file://, some things quietly fail: modules, fetch requests, and anything that needs a server. Use a local server. VS Code’s Live Server extension, or python -m http.server in the folder, then open http://localhost:8000.
The script tag itself#
<script src="app.js" /> <!-- wrong: self-closing script tags do not work -->
<script src="app.js"></script> <!-- right -->
<script src="app.js">
console.log('this inline code is ignored when src is set');
</script>
A self-closing script tag swallows everything after it until the next closing tag, which can silently eat half the page. And a tag with a src ignores its inline content; use two tags if you need both.
Questions people ask#
defer or async?
defer runs after parsing, in order. async runs as soon as it downloads, in any order, and is for independent scripts such as analytics. For your own code, defer.
Does the script need type=”text/javascript”?
No. It is the default and has been for years. Only type="module" matters.
Why does my onclick attribute say the function is not defined?
The function is inside a module, or the script has not loaded yet, or the name is misspelt. Prefer addEventListener in the script to attributes in the HTML.
The script works in Chrome but not Safari.
Check the console in Safari; enable the Develop menu in its settings first. Usually a newer language feature Safari’s version lacks, or a module served without the right MIME type.
Where to go next#
- “Cannot read properties of undefined” — the error from cause 1, in depth.
- Debugging JavaScript in the browser — the developer tools properly.
- JavaScript DOM basics — what the script is trying to do.