Selenium StaleElementReferenceException (tested)
Selenium raises StaleElementReferenceException: stale element reference when the DOM node your WebElement pointed to was replaced or removed, typically by a re-render, a reload or a navigation. Find the element again after the page changes; to wait for that change, use EC.staleness_of(old_element).
Setup used in every block
common.py writes a small HTML page to a local file and starts headless Chrome. Exception messages are cut to their first line (Selenium appends the session info and a documentation link).
import os, pathlib
from selenium import webdriver
CHROME = os.path.expanduser("~/.cache/ms-playwright/chromium-1248/chrome-linux64/chrome")
def page(html):
p = pathlib.Path("/tmp/sl383/page.html"); p.write_text(html); return p.as_uri()
def driver():
o = webdriver.ChromeOptions()
o.binary_location = CHROME
o.add_argument("--headless=new"); o.add_argument("--no-sandbox"); o.add_argument("--window-size=1000,700")
return webdriver.Chrome(options=o)
1. Reproduce it: the page replaced the element
from common import *
from selenium.webdriver.common.by import By
d = driver()
d.get(page('<input id="q"><button id="b" onclick="document.body.innerHTML=document.body.innerHTML">re-render</button>'))
q = d.find_element(By.ID, "q")
d.find_element(By.ID, "b").click() # page code replaces the DOM nodes
try:
q.send_keys("hello")
except Exception as e:
print(type(e).__name__ + ":", e.msg.splitlines()[0])
q = d.find_element(By.ID, "q") # find it again
q.send_keys("hello")
print("re-found:", q.get_attribute("value"))
d.refresh()
try:
q.get_attribute("value")
except Exception as e:
print("after refresh():", type(e).__name__)
d.quit()
Output:
StaleElementReferenceException: stale element reference: stale element not found in the current frame
re-found: hello
after refresh(): StaleElementReferenceException
A WebElement is a handle to one specific DOM node, not a saved locator. When page code replaces that node (a framework re-render, innerHTML, a reload, a navigation), the handle points to a node that no longer exists and any call on it raises StaleElementReferenceException, even though an identical-looking element with the same id is now on the page. The fix is to call find_element() again after the change. The exact message differs between chromedriver versions; Chrome 156 says stale element not found in the current frame.
2. The classic: clicking through a list of elements
from common import *
from selenium.webdriver.common.by import By
html = '''<a href="#a">A</a> <a href="#b">B</a> <a href="#c">C</a>
<script>onhashchange = () => { document.body.innerHTML = document.body.innerHTML; }</script>'''
d = driver()
d.get(page(html))
try:
for link in d.find_elements(By.TAG_NAME, "a"):
print("clicking", link.text)
link.click()
except Exception as e:
print(type(e).__name__)
d.get(page(html))
for i in range(len(d.find_elements(By.TAG_NAME, "a"))):
link = d.find_elements(By.TAG_NAME, "a")[i] # re-find on every iteration
print("clicking", link.text)
link.click()
print("done")
d.quit()
Output:
clicking A
StaleElementReferenceException
clicking A
clicking B
clicking C
done
The first click makes the page re-render, so every other element in the list you collected up front is stale. Re-find the elements inside the loop (by index, as here), or collect what you need first (for links, [a.get_attribute("href") for a in links]) and then visit those URLs with driver.get().
3. Wait for the update instead of racing it
from common import *
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import StaleElementReferenceException
d = driver()
d.get(page('''<div id="list"><p class="row">old</p></div>
<button id="load" onclick="setTimeout(() => document.getElementById('list').innerHTML = '<p class=row>new</p>', 1000)">load</button>'''))
old = d.find_element(By.CLASS_NAME, "row")
d.find_element(By.ID, "load").click()
print("text right after click:", d.find_element(By.CLASS_NAME, "row").text)
WebDriverWait(d, 5).until(EC.staleness_of(old)) # wait until the old node is gone
print("after staleness_of:", d.find_element(By.CLASS_NAME, "row").text)
wait = WebDriverWait(d, 5, ignored_exceptions=[StaleElementReferenceException])
print("ignored_exceptions:", wait.until(lambda d: d.find_element(By.CLASS_NAME, "row").text))
d.quit()
Output:
text right after click: old
after staleness_of: new
ignored_exceptions: new
Right after the click the old content is still there, so reading immediately gives the old text, and a re-render a moment later makes your handle stale. EC.staleness_of(old_element) waits until the old node is detached, which is a reliable "the page has updated" signal; re-find after it. For a polling wait whose lambda calls find_element() fresh each time, ignored_exceptions=[StaleElementReferenceException] makes the wait retry if the node is replaced between the find and the read, instead of failing.
4. Checklist
- Do not keep
WebElements across actions that change the page; keep the locator ((By.ID, "q")) and re-find. - Loops: re-find per iteration or collect hrefs/text first.
- After a click that loads content:
EC.staleness_of(old), then re-find. - Avoid wrapping every call in a blind retry: it hides real bugs like clicking before the page finished updating.
Related: Selenium element not interactable, Selenium element click intercepted.
Frequently asked questions
What causes StaleElementReferenceException in Selenium?
The page replaced or removed the DOM node your WebElement refers to (re-render, innerHTML, refresh, navigation). The handle does not follow the new node, even with the same id.
How do I fix stale element reference in a loop?
Re-find the elements inside the loop (e.g. by index) or collect hrefs/text before clicking, instead of reusing elements collected before the first click.
Written with AI assistance. Measured on 2026-10-23 with Selenium 4.51.0 (Python 3.13.5) and headless Chrome for Testing 156.0.8078.4. Each block was run verbatim and the output pasted unedited. PlainHeaders is our extension; Python, Selenium and Chrome are free.