In dynamic web applications (like those using HTMX, Hotwire, Turbo or React), there is this classic "stale reference" issue.

As a quick example, consider the following javascript code that finds an element after receiving an event, and then deletes that element after 5 seconds.

  document.addEventListener("elementSwapped", () => {
      const timestampElements = document.querySelectorAll('[id^="el-last-saved-"]');
      timestampElements.forEach(timestampElement => {
  		if (!timestampElement.dataset.clearPending) {
  			timestampElement.dataset.clearPending = 'true';
  			setTimeout(() => {
  				timestampElement.remove();
  			}, 5000);
  		};
      });
  });

While the logic is sound, the element reference el captured in setTimeout is likely becoming outdated before the 5 seconds are up.

What happens is that when you save an element into a variable (const el), you are saving a reference to that specific node in memory.

If your framework refreshes that part of the HTML within those 5 seconds, it destroys the old node and puts a new, identical-looking node in its place.

  1. 0s: You find the element.
  2. 2s: The page/form updates. The element is removed from the DOM, and a copy is added.
  3. 5s: The timer fires. It calls .remove() on the old element, which is already gone/detached. The new copy is on the screen.

The Solution

Instead of relying on the old variable, search for the element again right before deleting it. This ensures you are deleting the element currently shown on the screen.

  document.addEventListener("elementSwapped", () => {
      const timestampElements = document.querySelectorAll('[id^="el-last-saved-"]');
      timestampElements.forEach(timestampElement => {
  		if (!timestampElement.dataset.clearPending) {
  			timestampElement.dataset.clearPending = 'true';
  			const elementId = timestampElement.id;
  			setTimeout(() => {
  				const currentElement = document.getElementById(elementId);
  				currentElement.remove();
  			}, 5000);
  		};
      });
  });