Script Tag Positioning: Common Mistakes and How to Fix Them

Suppose you are creating a webpage where you increment a number displayed on the screen by pressing a button.

You wrote the following code, but it doesn’t work—nothing happens when you press the button:

<!DOCTYPE html>
<html>
  <head>
    <title>Counter</title>
    <link rel="stylesheet" href="index.css" />
  </head>
  <body>
    <script src="index.js"></script>
    <h1 id="counter"></h1>
    <button onclick="increment()">Increment</button>
  </body>
</html>
let count = 0;
let countEl = document.getElementById("counter");
function increment() {
  count += 1;
  countEl.innerText = count;
}

However, when you change the countEl.innerText line to use the counter ID directly, as shown below, the code works:

let count = 0;
let countEl = document.getElementById("counter");
function increment() {
  count += 1;
  counter.innerText = count; // Using 'counter' directly
}

Can you spot the issue?

The problem lies in the timing of when the script executes relative to when the DOM elements are created.

In the first version of the code, the <script> tag is placed before the <h1> element it tries to access.

When the script runs, it immediately attempts to find the element with the ID counter. At that point, the element hasn’t been created yet, so document.getElementById("counter") returns null, and countEl remains null forever.

As a result, the increment function fails to update anything on the screen.

In the second version of the code, it works because the id="counter" attribute creates a property on the window object with the same name (window.counter).

This property is available even if the element doesn’t exist when the script executes. However, relying on this behavior is considered bad practice and should be avoided.

To fix the issue properly, you should ensure the script runs only after the DOM is fully loaded. There are a few ways to do this:

  • Move the <script> tag to the end of the <body>:

    <body>
      <h1 id="counter"></h1>
      <button onclick="increment()">Increment</button>
      <script src="index.js"></script>
    </body>
    

    This ensures the script runs after all elements have been created.

  • Use the defer attribute in the <script> tag:

    <script src="index.js" defer></script>
    

    This tells the browser to wait until the entire HTML document is parsed before running the script.

  • Add type="module" to the <script> tag:

    <script src="index.js" type="module"></script>
    

    Using type="module" not only treats the script as a module (which is recommended for modern JavaScript), but also defers its execution until the DOM is fully loaded.

Always ensure your JavaScript runs after the DOM is fully loaded to avoid issues like trying to access elements that don’t yet exist. Avoid relying on window properties created for id attributes, as this behavior is unpredictable and can cause conflicts.

Get my free, weekly JavaScript tutorials

Want to improve your JavaScript fluency?

Every week, I send a new full-length JavaScript article to thousands of developers. Learn about asynchronous programming, closures, and best practices — as well as general tips for software engineers.

Join today, and level up your JavaScript every Sunday!

Thank you, Taha, for your amazing newsletter. I’m really benefiting from the valuable insights and tips you share.

- Remi Egwuda