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

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.