What you'll be able to do
By the end of this page you should be able to fix “cannot read property of null” from early scripts, use DOMContentLoaded, and choose defer vs async on <script src> tags with a clear mental model.
Previous post: event delegation.
Who this is for
- People who moved script to
<head>and brokequerySelector - Beginners loading external
.jsfiles - Anyone comparing defer/async charts online without context
You can skip this if script placement never bites you. Come back when external bundles load in the wrong order.
DOMContentLoaded
document.addEventListener("DOMContentLoaded", function () {
const btn = document.querySelector("#go"); // safe if #go is in HTML above
});
Scenario A — script in <head>. Wait for DOM parsed, not for every image.
Scenario B — end-of-body script. Often you do not need this wrapper — HTML below already exists.
defer on external scripts
<script defer src="app.js"></script>
Scenario A — app.js in <head>. Downloads in parallel; runs after HTML parsed; order preserved between defer scripts.
Scenario B — multiple defer files. They run in document order.
async: load now, run when ready
<script async src="analytics.js"></script>
Scenario A — independent snippet (analytics). Order vs other async scripts is not guaranteed.
Scenario B — app logic that needs DOM. Prefer defer, not async.
| Attribute | DOM ready? | Order with other scripts |
|---|---|---|
| none (blocking) | runs immediately when hit | blocking |
| defer | after parse | preserved |
| async | when downloaded | not preserved |
Inline script at end of body (still valid)
From post #41: placing <script> after elements is the simplest teaching habit. defer + external file is the scalable habit.
A tiny practice file
Save as dom-ready.html.
<!DOCTYPE html>
<html>
<head>
<script>
document.addEventListener("DOMContentLoaded", function () {
document.getElementById("status").textContent = "DOM ready";
});
</script>
</head>
<body>
<p id="status">Waiting…</p>
</body>
</html>
Mistakes I see a lot
1. async on main app bundle — race with DOM or other scripts.
2. DOMContentLoaded inside end-of-body without reason — extra noise.
3. Confusing DOMContentLoaded with load (all images too).
4. Module scripts — type="module" is defer by default (post #51).
What to try before the next post
- Break early script; fix with DOMContentLoaded.
- Compare defer vs async with two external files (log order).
- Build
dom-ready.html.
Next in this series: data attributes — data-* instead of hardcoded ids everywhere.
Try this next outside the series
Browser JavaScript stops feeling isolated once you connect it to forms and component-oriented UI work.
- React forms without pain — see modern form handling after you understand the DOM version first
- HTML to JSX Converter — move tiny HTML snippets into React-style markup when you're ready