javascriptjquerydom

Why does jQuery or a DOM method such as getElementById not find the element?


What are the possible reasons for document.getElementById, $("#id") or any other DOM method / jQuery selector not finding the elements?

Example problems include:

Uncaught TypeError: Cannot set property '...' of null
Uncaught TypeError: Cannot set properties of null (setting '...')
Uncaught TypeError: Cannot read property '...' of null
Uncaught TypeError: Cannot read properties of null (reading '...')

The most common forms are:

Uncaught TypeError: Cannot set property 'onclick' of null
Uncaught TypeError: Cannot read property 'addEventListener' of null
Uncaught TypeError: Cannot read property 'style' of null


Solution

  • The element you were trying to find wasn’t in the DOM when your script ran.

    The position of your DOM-reliant script can have a profound effect on its behavior. Browsers parse HTML documents from top to bottom. Elements are added to the DOM and scripts are (by default) executed as they're encountered. This means that order matters. Typically, scripts can't find elements that appear later in the markup because those elements have yet to be added to the DOM.

    Consider the following markup; script #1 fails to find the <div> while script #2 succeeds:

    <script>
      console.log("script #1:", document.getElementById("test")); // null
    </script>
    <div id="test">test div</div>
    <script>
      console.log("script #2:", document.getElementById("test")); // <div id="test" ...
    </script>

    So, what should you do? You've got a few options:


    Option 1: Move your script

    Given what we've seen in the example above, an intuitive solution might be to simply move your script down the markup, past the elements you'd like to access. In fact, for a long time, placing scripts at the bottom of the page was considered a best practice for a variety of reasons. Organized in this fashion, the rest of the document would be parsed before executing your script:

    <body>
      <button id="test">click me</button>
      <script>
        document.getElementById("test").addEventListener("click", function() {
          console.log("clicked:", this);
        });
      </script>
    </body><!-- closing body tag -->

    While this makes sense, and is a solid option for legacy browsers, it's limited and there are more flexible, modern approaches available.


    Option 2: The defer attribute

    While we did say that scripts are, "(by default) executed as they're encountered," modern browsers allow you to specify a different behavior. If you're linking an external script, you can make use of the defer attribute.

    [defer, a Boolean attribute,] is set to indicate to a browser that the script is meant to be executed after the document has been parsed, but before firing DOMContentLoaded.

    This means that you can place a script tagged with defer anywhere, even the <head>, and it should have access to the fully realized DOM.

    <script src="https://gh-canon.github.io/misc-demos/log-test-click.js" defer></script>
    <button id="test">click me</button>

    Just keep in mind...

    1. defer can only be used for external scripts, i.e.: those having a src attribute.
    2. be aware of browser support, i.e.: buggy implementation in IE < 10

    Option 3: Modules

    Depending upon your requirements, you may be able to utilize JavaScript modules. Among other important distinctions from standard scripts (noted here), modules are deferred automatically and are not limited to external sources.

    Set your script's type to module, e.g.:

    <script type="module">
      document.getElementById("test").addEventListener("click", function(e) {
        console.log("clicked: ", this);
      });
    </script>
    <button id="test">click me</button>


    Option 4: Defer with event handling

    Add a listener to an event that fires after your document has been parsed.

    DOMContentLoaded event

    DOMContentLoaded fires after the DOM has been completely constructed from the initial parse, without waiting for things like stylesheets or images to load.

    <script>
      document.addEventListener("DOMContentLoaded", function(e){
        document.getElementById("test").addEventListener("click", function(e) {
          console.log("clicked:", this);
        });
      });
    </script>
    <button id="test">click me</button>

    Window: load event

    The load event fires after DOMContentLoaded and additional resources like stylesheets and images have been loaded. For that reason, it fires later than desired for our purposes. Still, if you're considering older browsers like IE8, the support is nearly universal. Granted, you may want a polyfill for addEventListener().

    <script>
      window.addEventListener("load", function(e){
        document.getElementById("test").addEventListener("click", function(e) {
          console.log("clicked:", this);
        });
      });
    </script>
    <button id="test">click me</button>

    jQuery's ready()

    DOMContentLoaded and window:load each have their caveats. jQuery's ready() delivers a hybrid solution, using DOMContentLoaded when possible, failing over to window:load when necessary, and firing its callback immediately if the DOM is already complete.

    You can pass your ready handler directly to jQuery as $(handler), e.g.:

    <script src="https://code.jquery.com/jquery-3.6.0.js" integrity="sha256-H+K7U5CnXl1h5ywQfKtSj8PCmoN9aaq30gDh27Xc0jk=" crossorigin="anonymous"></script>
    <script>
      $(function() {
        $("#test").click(function() {
          console.log("clicked:", this);
        });
      });
    </script>
    <button id="test">click me</button>


    Option 5: Event Delegation

    Delegate the event handling to an ancestor of the target element.

    When an element raises an event (provided that it's a bubbling event and nothing stops its propagation), each parent in that element's ancestry, all the way up to window, receives the event as well. That allows us to attach a handler to an existing element and sample events as they bubble up from its descendants... even from descendants added after the handler was attached. All we have to do is check the event to see whether it was raised by the desired element and, if so, run our code.

    Typically, this pattern is reserved for elements that don't exist at load time or to avoid attaching a large number of duplicate handlers. For efficiency, select the nearest reliable ancestor of the target element rather than attaching it to the document. If appropriate, don't forget Event:stopPropagation() to prevent further bubbling/processing.

    Native JavaScript

    <div id="ancestor"><!-- nearest ancestor available to our script -->
      <script>
        document.getElementById("ancestor").addEventListener("click", function(e) {
          if (e.target.id === "descendant") {
            e.stopPropagation(); // stop bubbling
            console.log("clicked:", e.target);
          }
        });
      </script>
      <button id="descendant">click me</button>
    </div>

    jQuery's on()

    jQuery makes this functionality available through on(). Given an event name, a selector for the desired descendant, and an event handler, it will resolve your delegated event handling and manage your this context:

    <script src="https://code.jquery.com/jquery-3.6.0.js" integrity="sha256-H+K7U5CnXl1h5ywQfKtSj8PCmoN9aaq30gDh27Xc0jk=" crossorigin="anonymous"></script>
    <div id="ancestor"><!-- nearest ancestor available to our script -->
      <script>
        $("#ancestor").on("click", "#descendant", function(e) {
          e.stopPropagation(); // stop bubbling
          console.log("clicked:", this);
        });
      </script>
      <button id="descendant">click me</button>
    </div>