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 broke querySelector
  • Beginners loading external .js files
  • 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.

AttributeDOM ready?Order with other scripts
none (blocking)runs immediately when hitblocking
deferafter parsepreserved
asyncwhen downloadednot 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 scriptstype="module" is defer by default (post #51).

What to try before the next post

  1. Break early script; fix with DOMContentLoaded.
  2. Compare defer vs async with two external files (log order).
  3. Build dom-ready.html.

Next in this series: data attributesdata-* 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.