FIX - addEventListener cannot read property of null

Guide to fix the addEventListener cannot read property of null issue

Jul 5, 2023 | Read time 9 minutes

🔔 Table of contents

Introduction

When you get “addEventListener cannot read property of null” means that you are trying to attach a event listener (eg ‘click’, ‘mouseover’, etc) to a null object.

The object can be null due to the DOM not being fully loaded yet or that you are providing the wrong selector.

Another similar error could look like:

Uncaught TypeError: Cannot read properties of null (reading 'addEventListener') at ...

I came across this issue multiple times in various projects and have a checklist on things to look out for when this happens.

Essentially we want to consider:

  1. The code is targeting the right element
  2. Wrap your code in DOMContentLoaded
  3. Add null checks before calling addEventListener
  4. Consider using the async or defer scripts
  5. Place the script tag at the end </body>

1. Make sure to target the right element

First check your HTML code and make sure the element you’re trying to attach an event listener to actually exists.

Additionally make sure there are no typos in the ID or class name of the element.

For example, I have made a few mistakes like this in the past:

// ❌ failed because there are multiple '.small-button' elements and querySelector only retrieves the first one
document.querySelector(".small-button").addEventListener('click', function() {
    // do something
});

// ❌ failed because querySelectorAll returns a list - and addEventLister is available for single elements
document.querySelectorAll(".small-button").addEventListener('click', function() {
    // do something
});

// ❌ failed because the hash '#' is not needed
document.getElementById("#mybutton").addEventListener('click', function() {
    // do something
});

2. Wrap your code in DOMContentLoaded

A common mistake is to put the JavaScript code is in the head of the HTML document. This forces it to be executing before the HTML elements are fully loaded (given that you are using a simple script tag with no attributes).

We can get around this by using the DOMContentLoaded event to wait until the HTML is fully loaded before running the JavaScript code.

An example to do this is as follows:

document.addEventListener("DOMContentLoaded", function(){
    // your code here
    var myElement = document.getElementById('myElement');
    myElement.addEventListener('click', function() {
        console.log('Element was clicked!');
    });
});

With the above, the DOMContentLoaded event will ensure that the entire page is loaded before the script tries to attach the event listener.

This event fires when the HTML document has been completely loaded and parsed, including the downloading and execution of deferred scripts, marked by the defer attribute in <script> tags or scripts of type module.

It’s important to note that DOMContentLoaded doesn’t wait for elements like:

  • images,
  • subframes, or
  • scripts marked with the async attribute.

It also doesn’t wait for stylesheets to load, though interestingly, deferred scripts do wait for stylesheets before they execute.

Furthermore, non-deferred or non-async scripts will wait for stylesheets to load before they execute.

This sequencing of events is key to understanding when and how different parts of your page are ready.

The DOMContentLoaded event is targeted at the Document object of the loaded page.

However, you can listen for it on the Window interface to handle it in the capture or bubbling phases.

Contrastingly, the load event indicates when the entire page, including all dependent resources like images and stylesheets, is fully loaded.

It’s a common mistake to use load in places where DOMContentLoaded would be more appropriate, as the former waits for all resources to load, not just the initial HTML document and certain scripts.

3. Add null checks before calling addEventListener

One good practice if you know you could get this error is to check if the HTML/DOM element is null in the first place.

var el = document.getElementById('overlayBtn');
el && el.addEventListener('click', swapper, false);

For a more modern approach we can us the optional chaining operation in JS (ES2020) - although it might not be support with older browsers:

 var el = document.getElementById('overlayBtn');
 el?.addEventListener('click', swapper, false);

The ?. is known as the optional chaining operator. It is a feature introduced in later versions of JavaScript (ES2020). What it essentially does is to check if el is not null or undefined before attempting to access the addEventListener method on el.

If el is null or undefined, then el?.addEventListener() will evaluate to undefined, and no error will be thrown.

Tip consider dynamic elements

If your element is dynamically created or being loaded asynchronously (for example, being fetched from a server), it might not exist at the time the JavaScript code is being run.

In that case, you would need to ensure that your code runs only after the element has been fully loaded. This can be tricky, but generally, you’d use some kind of callback or promise to handle it.

4. Consider using the async or defer scripts

HTML5 gives us two attributes that we can use on the <script> tag - async and defer.

Generally, with async the Javascript file will be downloaded asynchronously and will execute once its downloaded.

With defer it will also be downloaded asynchronously, but will only execute when the document has fully loaded.

We can use the async attribute like so:

<html>
    <body>

        ...
        ...
        <script src="myScript.js" async></script>
    </body>
</html>

And the defer attribute as follows:

<html>
    <body>

        ...
        ...
        <script src="myScript.js" defer></script>
    </body>
</html>

Now if you have inline scripts, then these attributes have no effect - it only works when you are loading a external script using the src attribute.

Additionally, the defer attribute keeps the script order!

<html>
    <body>

        ...
        ...
        <script src="myScriptA.js" defer></script>
        <script src="myScriptB.js" defer></script>
        <script src="myScriptC.js" defer></script>
    </body>
</html>

So in the above, the scripts will be downloaded asynchronously, and will execute in the order they are given in the HTML. So in this case, the order of execution is:

  1. myScriptA.js
  2. myScriptB.js
  3. myScriptC.js

Browser compatibility with async or defer scripts

  • Generally the async and defer attributes on the script tag is support for most modern browsers
  • If you are using IE version 9 or lower then you are out of luck - you will need to ignore this step.

5. Place the script tag at the end </body>

Now if you want to have maximum browser support then consider placing the script tag just before the </body> end tag.

For example:

<html>
    <body>

        ...
        ...
        <script src="myScript.js"></script>
    </body>
</html>

Browsers will render HTML sequentially from the top to the bottom.

Now when it sees a script tag in the head tag (or anywhere else within the HTML file) it will have download that script and execute it. This is usually why Javascript is called a blocking operation. It blocks everything else until the script is done.

This is why we can see that the addEventListener throw the null error - because the script is executing before the HTML element you are trying to target has not loaded yet!

By placing scripts at the end, you allow the browser to render most of your page before it encounters any JavaScript - hopefully will fix your error.

Tip: Loading JS in the end is good practice too!

Loading Javascript in the end of your HTML is usually good practice, since it gives the perception that the page loads faster.

  • No Javascript is blocking the render.
  • JavaScript is not blocking CSS from being parsed and applied, which can delay rendering and make your page appear to load slower.

Summary

The “addEventListener cannot read property of null” means that we are trying to call the function to a null DOM object.

Now this can happen when we are calling the function too early when the DOM object does not exist yet or targeting the wrong element.

To get around this issue the usual fix is to consider one of the following options:

  • wrap your code around the DOMContentLoaded event,
  • place the executing script at the end of the </body> tag
  • consider using the async or defer attributes
  • make sure you are checking for nulls before calling addEventListener

👋 About the Author

G'day! I am Huy a software engineer based in Australia. I have been creating design-centered software for the last 10 years both professionally and as a passion.

My aim to share what I have learnt with you! (and to help me remember 😅)

Follow along on Twitter , GitHub and YouTube