A Problem

Two common patterns in front-end JavaScript development conspire to cause a problem:

1) We attach event handlers to page elements in the DOM with code like:

function handleClick(event) {
    // handles a click event
};
const elem = document.querySelector("#myelement");
elem.addEventListener('click', handleClick);

2) We often load JavaScript with <script> tags in the <head> element of a page:

<html>
  <head>
    ...
    <script src="app.js"></script>
  </head>
  <body>
    ...
  </body>
</html>

The problem arises because the code in our script is parsed and executed when the browser reaches the <script> tag in the page. At that time, the browser has not yet reached the <body> element, and so the DOM hasn’t been populated with page elements yet. Therefore, when the browser executes document.querySelector(...), it returns nothing (technically, it returns null), because there is no element to find (yet)!

This test page on JSFiddle shows this. The event handler is not successfully attached to the button, and if you dig in the console you’ll see an error: TypeError: elem is null

A Solution

To solve this, the browser needs to wait before it executes any code that is working with DOM elements. It needs to wait until after it has fully loaded the DOM. Helpfully, there is an event we can use just for this: DOMContentLoaded. It will fire after the browser has loaded and parsed the body of the page, populating the DOM.

If we put any code that attaches events to DOM elements inside a function, we can attach that function to the DOMContentLoaded event. It will look something like this:

// A function for attaching events to DOM elements
function setupEvents() {
  const elem = document.querySelector("#myelement");
  elem.addEventListener('click', handleClick);
};

// Attach our setup function to DOMContentLoaded
document.addEventListener('DOMContentLoaded', setupEvents);

Now, we won’t be trying to access elements in the DOM (using querySelector or similar functions) until after the DOM has finished loading.

This test page on JSFiddle shows this working, with the event handler successfully added to the button. Try it out yourself!