JavaScript: How to Wait for an Element to Exist

As web applications become more dynamic and asynchronous, it’s increasingly common to need to wait for elements to exist in the DOM before interacting with them. Trying to find or act on an element that doesn’t yet exist will result in errors and undefined behavior. In this comprehensive guide, we’ll explore the various methods available in modern JavaScript to wait for an element to exist before continuing execution.

The Problem: Dealing with Asynchronous DOM Updates

With the widespread adoption of asynchronous JavaScript patterns like Ajax and front-end frameworks like React, Angular, and Vue.js, the DOM structure of web apps is no longer static. Elements can get added, removed, or updated at any time, often in response to asynchronous operations like data fetching.

This means you can no longer rely on elements existing in the DOM when your JavaScript code runs. A classic example is trying to find an element that gets added after an Ajax request completes:

// BAD: tries to find element before Ajax request finishes
const element = document.getElementById('my-element'); 
// element will be null because #my-element doesn't exist yet

Acting on an element that isn’t yet present will lead to null references and Cannot read properties of null errors. So how do we synchronously wait for elements to exist before interacting with them?

Wait For Element To Exist Before Acting On It

The key is to wait until the element is present and rendered before trying to get a reference to it or act on it. Here are some common techniques to wait for an element to exist:

1. SetTimeout with Delay

The simplest approach is to just wait for a fixed time interval before checking if the element exists. This takes advantage of setTimeout to asynchronously delay code execution:

// Wait 1 second before checking
setTimeout(() => {
  const element = document.getElementById('my-element');
  // Manipulate element
}, 1000);

This works well if you know a fixed amount of time to wait for the element to appear. But it’s non-ideal because we have to guess at a delay, and it will fail if the element takes longer to load.

2. setInterval with Repeated Checks

To avoid guessing a fixed delay, we can repeatedly check for the element’s existence on an interval using setInterval:

let element = null;

const interval = setInterval(() => {
  element = document.getElementById('my-element');
  if (element) {
    // Element exists, do something with it  
    clearInterval(interval); 
  } 
}, 200); // Check every 200ms

This polls for the element to appear every 200ms. If found, it clears the interval timer so we don’t waste resources with continued checking.

The downside is it burns CPU cycles continuously checking, even when the element doesn’t yet exist. There are more efficient methods.

3. Recursive setTimeout

We can improve efficiency by using recursive setTimeout calls instead of setInterval. This schedules a new timeout only when the element doesn’t exist, avoiding constant polling:

const waitForElement = (selector, callback) => {
  const element = document.querySelector(selector);
  if (element) {
    callback(element); // Element found, run callback
  } else {
    // Element not found, check again after 200ms
    setTimeout(() => {
      waitForElement(selector, callback); 
    }, 200)
  }
}

// Usage:
waitForElement('#my-element', element => {
  // Do something with element
});

This recurses until the element is found, without repeated checking. The timeout delay can also be increased after each check, using techniques like exponential backoff to avoid wasting cycles.

4. Event Listeners

If we know what event causes the element to be added, we can listen for that event and wait to act until the handler fires:

// Wait for element to be added after ajax load
document.addEventListener('ajaxLoaded', () => {
  const element = document.querySelector('#my-element');
  // Element exists
});

This guarantees the element exists once the handler runs. The downside is we need access to the event/component that renders the element.

5. MutationObserver

For more complex cases where we don’t know the exact rendering event, we can listen for general DOM changes using a MutationObserver.

// Observe entire document for added nodes
const observer = new MutationObserver(mutations => {
  mutations.forEach(mutation => {
    if (mutation.addedNodes) {
      const element = document.querySelector('#my-element');
      if (element) {
        // Element exists in the DOM
      }
    } 
  });    
});

observer.observe(document, {
  childList: true, 
  subtree: true
});

Mutation observers can watch the entire DOM and fire callbacks on any changes, letting us detect exactly when an element gets rendered without specific knowledge of the events.

The browser support is fairly good but falls back to the older Mutation Events in IE10 and below.

Avoid Errors by Checking Element Existence First

All of these techniques share a common pattern – before interacting with an element, always check that it exists in the DOM first:

// BAD: throws error if element doesn't exist
const element = document.getElementById('my-element');
element.style.color = 'blue'; 

// GOOD: avoid errors by checking existence first
const element = document.getElementById('my-element');
if (element) {
  element.style.color = 'blue';
}

This simple check protects against undefined and null reference errors even if the wait methods fail for any reason. Defensively checking for presence before acting on an element is a good habit.

Choosing the Right Method

There are tradeoffs to each approach. Here are some guidelines on which method to choose:

  • setTimeout – Simple but inefficient if the element takes long to appear. Good for quick, short waits.
  • setInterval – Allows continuous checking but wastes resources if element takes long to load.
  • Recursive setTimeout – More efficient than setInterval but requires recursive function.
  • Event Listeners – Guaranteed to wait for element with good performance. Requires knowledge of what events are rendering element.
  • MutationObserver – Robust waiting for any DOM change but complex to set up. Good when events are unknown.

Also consider browser support tradeoffs. MutationObserver has good support but falls back to Mutation Events in old IE. setInterval and setTimeout are supported everywhere.

For quick, simple cases, a setTimeout is usually sufficient. For complex apps with dynamic interfaces, the added complexity of MutationObserver may be worth it.

Common Use Cases

Here are some common use cases where waiting for element existence is necessary:

  • Waiting for results of an AJAX request to render
  • Waiting for images or other media to load before acting on them
  • Waiting for asynchronously loaded ads/widgets to appear before interacting
  • Waiting for animations or transitions to complete before measuring DOM elements
  • Waiting for React/Angular/Vue components to mount in the DOM
  • Integration with third party apps that update the DOM asynchronously

Any time part of the page structure loads asynchronously, you likely need to wait before interacting with it.

Anti-Patterns to Avoid

Some anti-patterns should be avoided when dealing with asynchronous DOM updates:

  • Excessive timeouts – Don’t use timeouts longer than necessary. Start with short intervals 100-500ms and increase gradually as needed.
  • Polling when events exist – Use events like ajaxComplete rather than inefficient polling if the element creation events are known.
  • Querying too often – Check for element existence once rather than in a tight loop to avoid wasting cycles.
  • Interacting before confirming existence – Always check for existence before reading or writing properties to avoid errors.
  • Ignoring browser support – Use feature detection and fallbacks, especially for newer APIs like MutationObserver.
  • Stopping after first element found – Use specific IDs/selectors rather than stopping on first matching element.

Following performance best practices avoids pitfalls like excessive resource usage, erroneous interactions, and unintended elements.

Conclusion

Asynchronous DOM updates are common in modern web apps. To reliably interact with elements:

  • Use timers, observers or events to wait for elements to exist.
  • Check for element presence before manipulating to avoid errors.

Leave a Comment