Question
What are the possible reasons for document.getElementById
, $("#id")
or any
other DOM method / jQuery selector not finding the elements?
Example problems include:
- jQuery silently failing to bind an event handler
- jQuery "getter" methods (
.val()
,.html()
,.text()
) returningundefined
- A standard DOM method returning
null
resulting in any of several errors:
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
Answer
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
](https://developer.mozilla.org/en-
US/docs/Web/HTML/Element/script#attr-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
](https://developer.mozilla.org/en- US/docs/web/api/window/domcontentloaded_event).
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...
defer
can only be used for external scripts, i.e.: those having asrc
attribute.- 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](https://developer.mozilla.org/en- US/docs/Web/JavaScript/Guide/Modules#applying_the_module_to_your_html). Among other important distinctions from standard scripts ([noted here](https://developer.mozilla.org/en- US/docs/Web/JavaScript/Guide/Modules#other_differences_between_modules_and_standard_scripts)), 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
](https://developer.mozilla.org/en-
US/docs/web/api/window/domcontentloaded_event) 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
](https://developer.mozilla.org/en-
US/docs/Web/API/Window/load_event) 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](https://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-flow-
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
.
Native JavaScript
<div id="ancestor"><!-- nearest ancestor available to our script -->
<script>
document.getElementById("ancestor").addEventListener("click", function(e) {
if (e.target.id === "descendant") {
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) {
console.log("clicked:", this);
});
</script>
<button id="descendant">click me</button>
</div>