Introduction
org.openqa.selenium.StaleElementReferenceException: stale element reference: stale element not found in the current frame is Selenium telling you that the WebElement you're holding no longer points at anything on the page. You found the element, then something removed it, and you used the old reference.
This page reproduces it on Sauce Demo, shows which actions replace elements there and which don't, and tests the common fixes, including the catch-and-retry loop that many answers recommend. Everything was run on 13 October 2026 with Selenium 4.49.0, JUnit 6.1.3 and Java 21 against Chrome 154, headless, at 1280 × 720. The project setup is the one from getting started with Selenium in Java.
What makes an element stale
A WebElement is a reference to one node in the page. Selenium doesn't store a way to find it again, only the node. When the page removes that node, the reference is stale, even if an identical-looking element appears in the same place. Selenium's errors page lists the usual causes: a refresh, navigating away, switching frames or windows, and the page changing its own content.
The last one is the one that surprises people, because nothing in the test looks like a page change. On Sauce Demo, a React app, these two actions replaced elements:
- Choosing a sort order replaced the sort dropdown itself and every "Add to cart" button.
- Clicking "Add to cart" replaced that button with a new "Remove" button.
We checked this in the browser by keeping a reference to each node, acting, and reading its isConnected property afterwards. The product cards and the product name nodes were not replaced by sorting: React moved the same nodes into the new order. That difference matters below.
The spec
The Maven dependencies:
<dependencies>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>4.49.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>6.1.3</version>
<scope>test</scope>
</dependency>
</dependencies>src/test/java/StaleTest.java has the four mistakes and the two fixes in one class:
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.StaleElementReferenceException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.Select;
import org.openqa.selenium.support.ui.WebDriverWait;
class StaleTest {
// Sauce Demo publishes this password on its own login page.
private static final String PASSWORD = System.getenv().getOrDefault("SAUCE_PASSWORD", "secret_sauce");
private static final By SORT = By.cssSelector("[data-test='product-sort-container']");
private static final By ADD_BACKPACK = By.cssSelector("[data-test='add-to-cart-sauce-labs-backpack']");
private static final By REMOVE_BACKPACK = By.cssSelector("[data-test='remove-sauce-labs-backpack']");
private static final By NAMES = By.cssSelector("[data-test='inventory-item-name']");
private static final By BADGE = By.cssSelector("[data-test='shopping-cart-badge']");
private WebDriver driver;
private WebDriverWait wait;
@BeforeEach
void logIn() {
ChromeOptions options = new ChromeOptions();
options.addArguments("--headless=new", "--window-size=1280,720");
// Sauce Demo's password is public, so Chrome's leaked-password check fires after login. Turn it off.
options.setExperimentalOption("prefs", Map.of("profile.password_manager_leak_detection", false));
driver = new ChromeDriver(options);
wait = new WebDriverWait(driver, Duration.ofSeconds(5));
driver.get("https://www.saucedemo.com/");
driver.findElement(By.id("user-name")).sendKeys("standard_user");
driver.findElement(By.id("password")).sendKeys(PASSWORD);
driver.findElement(By.id("login-button")).click();
wait.until(ExpectedConditions.presenceOfElementLocated(SORT));
}
@AfterEach
void quit() {
driver.quit();
}
@Test
void sortsTwiceWithOneSelect() {
Select sort = new Select(driver.findElement(SORT));
sort.selectByValue("lohi");
sort.selectByValue("hilo");
assertEquals("Sauce Labs Fleece Jacket", driver.findElements(NAMES).get(0).getText());
}
@Test
void addsThenRemovesWithOneButton() {
WebElement button = driver.findElement(ADD_BACKPACK);
button.click();
button.click();
assertEquals(0, driver.findElements(BADGE).size());
}
@Test
void readsNamesFoundBeforeSorting() {
List<WebElement> names = driver.findElements(NAMES);
new Select(driver.findElement(SORT)).selectByValue("lohi");
assertEquals("Sauce Labs Onesie", names.get(0).getText());
}
@Test
void retriesWithTheSameElement() {
WebElement button = driver.findElement(ADD_BACKPACK);
button.click();
for (int attempt = 1; ; attempt++) {
try {
button.click();
break;
} catch (StaleElementReferenceException e) {
if (attempt == 3) throw e;
}
}
assertEquals(0, driver.findElements(BADGE).size());
}
/** Runs the action again, up to three times, if an element it found went stale. */
private static void retryIfStale(Runnable action) {
for (int attempt = 1; ; attempt++) {
try {
action.run();
return;
} catch (StaleElementReferenceException e) {
if (attempt == 3) throw e;
}
}
}
@Test
void retriesWithAFreshLookup() {
new Select(driver.findElement(SORT)).selectByValue("lohi");
retryIfStale(() -> new Select(driver.findElement(SORT)).selectByValue("hilo"));
assertEquals("Sauce Labs Fleece Jacket", driver.findElements(NAMES).get(0).getText());
}
@Test
void findsEachElementAfterTheChange() {
new Select(driver.findElement(SORT)).selectByValue("lohi");
new Select(driver.findElement(SORT)).selectByValue("hilo");
assertEquals("Sauce Labs Fleece Jacket", driver.findElements(NAMES).get(0).getText());
driver.findElement(ADD_BACKPACK).click();
wait.until(ExpectedConditions.elementToBeClickable(REMOVE_BACKPACK)).click();
wait.until(ExpectedConditions.numberOfElementsToBe(BADGE, 0));
}
}We ran mvn test three times. Every run gave the same result: the four mistakes failed, and both fixes passed.
[ERROR] Tests run: 6, Failures: 1, Errors: 3, Skipped: 0
[ERROR] StaleTest.addsThenRemovesWithOneButton -- Time elapsed: 3.35 s <<< ERROR!
[ERROR] StaleTest.sortsTwiceWithOneSelect -- Time elapsed: 3.399 s <<< ERROR!
[ERROR] StaleTest.retriesWithTheSameElement -- Time elapsed: 3.452 s <<< ERROR!
[ERROR] StaleTest.readsNamesFoundBeforeSorting -- Time elapsed: 3.558 s <<< FAILURE!(The times include starting Chrome and logging in.)
Three ways to get it, and one way not to
1. A Select kept across a sort
The first selectByValue("lohi") worked. It also made Sauce Demo render a new dropdown, so the second call, on the same Select, failed:
org.openqa.selenium.StaleElementReferenceException:
stale element reference: stale element not found in the current frame
(Session info: chrome=154.0.8037.98)
For documentation on this error, please visit: https://www.selenium.dev/documentation/webdriver/troubleshooting/errors#staleelementreferenceexception
Build info: version: '4.49.0', revision: 'bedb24f bedb24f6f703876e7e4c5d506f0d697eb358c720'
...
Element: [[ChromeDriver: chrome on windows (77e7990a088abd2cc5ebf555784c84d0)] -> css selector: [data-test='product-sort-container']]
at StaleTest.sortsTwiceWithOneSelect(StaleTest.java:55)The Element: line is the useful one. It says which WebElement went stale and how it was found, here the sort dropdown by its data-test attribute. The stack trace line points at the second selectByValue, the first call made with the old reference after the page changed.
A Select wraps one WebElement, so keeping a Select in a field or a variable keeps the element too.
2. A button that the click replaced
addsThenRemovesWithOneButton clicked "Add to cart", then clicked the same WebElement again to remove the product. The first click replaced the button with a "Remove" button, so the second click failed with the same message, naming [data-test='add-to-cart-sauce-labs-backpack'].
The second click was wrong twice over. Even if the old node had still been there, the test meant to click "Remove", a different button with a different data-test.
3. A retry around the same element
retriesWithTheSameElement is the second click again, wrapped in the catch-and-retry loop that many answers to this error suggest: catch the exception and try again, up to three times. All three attempts failed and the test threw the same StaleElementReferenceException.
That's not bad luck. The node the reference points at has been removed, and it doesn't come back. Retrying button.click() repeats the same call on the same dead reference. Any retry has to call findElement again inside the loop.
4. Not stale, but wrong: names found before sorting
readsNamesFoundBeforeSorting found the six product names, sorted by price, and read the first name from the list it already had. There was no exception. It failed on the assertion:
org.opentest4j.AssertionFailedError: expected: <Sauce Labs Onesie> but was: <Sauce Labs Backpack>Sauce Demo moved the existing name nodes into the new order instead of replacing them, so every reference was still valid. But the Java List kept the order in which the elements were found, so names.get(0) was still the Backpack, now fifth on the page. A test that checks sorting this way can pass or fail for the wrong reason, and depending on how the app re-renders, the same code throws StaleElementReferenceException on one app and reads old data on another.
Is catch-and-retry the wrong fix?
We tested it before deciding. The broken form, retrying the same WebElement, failed every attempt and can't work, as above. The working form puts the lookup inside the retry, as retryIfStale(() -> new Select(driver.findElement(SORT)).selectByValue("hilo")) does. It passed in all three runs.
It also never retried. In those runs, and in 40 more on one browser that sorted and then looked the dropdown up again, a fresh findElement after the sort never went stale. The dropdown had already been replaced by the time Selenium looked for it, so the lookup found the new one. On this page the retry loop was code that did nothing, and findsEachElementAfterTheChange, which just finds each element after the action that replaces it, passed the same way with less to read.
So on the evidence here: a retry that re-finds the element isn't wrong, it's usually unnecessary. It's worth having where you can't tell when the page will re-render, for example a list that refreshes itself on a timer, because there the element can be replaced between your findElement and your click. Keep it small, retry only StaleElementReferenceException, and keep the lookup inside it. We didn't find a case on these pages where a retry hid a real bug, so we don't claim it does; but a retry that keeps firing tells you the page re-renders more than you thought, and that's worth knowing.
The fix
findsEachElementAfterTheChange is the fixed version of all three:
- Find the element after the action that replaces it.
new Select(driver.findElement(SORT))each time you choose a sort order. StoreBylocators in fields and page objects, notWebElements orSelects, so each method finds what it needs when it runs. - Click the element that's there now. After "Add to cart", the test waits for
REMOVE_BACKPACKand clicks that. The wait costs nothing when the button is already there, and covers an app that renders it a moment after the click. - Read values after the change. Find the names after sorting, not before.
A Chrome check that made clicks disappear
Our first runs of the fixed test failed, intermittently, with NoSuchElementException or a wait timing out on the "Remove" button. The click on "Add to cart" had returned without an error and done nothing. We added a script that logged every mouse event the page received: for the lost clicks there were none at all. Waiting a second before clicking made it worse: 0 of 12 clicks registered.
The cause was Chrome, not the page. Sauce Demo's password is public, and Chrome checks passwords you sign in with against known leaks; after login it offers to change it. Turning that check off with the Chrome preference profile.password_manager_leak_detection set to false fixed it. In 18 attempts with the check on, 2 clicks registered; with it off, 18 of 18 did. We can't see browser dialogs in a headless screenshot, so the dialog itself is inferred from that switch; the preference is now in the spec's @BeforeEach, with a comment saying why. If your Selenium clicks on a demo site sometimes do nothing and no exception is thrown, try this first.
How to prevent it
- Keep locators, not elements. A
Byin a field never goes stale; aWebElementin a field eventually will. - Know which of your actions re-render. Sorting, filtering, adding to a cart, saving a form and paginating usually do. After one of those, find elements again, and wait for the new state before you use it.
- Don't retry the same reference. If you add a retry, the lookup goes inside it, and it catches only
StaleElementReferenceException. - Check lists after the change, not before. A list of elements kept across a re-render is either stale or out of date, and the out-of-date case fails quietly.
Exercise
On Sauce Demo, add the Backpack and the Bike Light to the cart, open the cart, and keep a List<WebElement> of the cart's "Remove" buttons. Remove the Backpack, then click the second element of your list. Predict whether you get StaleElementReferenceException, a click on the wrong product, or neither, then run it and check which nodes the cart replaced.
The Playwright version of this error is "Element is not attached to the DOM"; that guide measures when Playwright's locators avoid it and when they don't. For the other click error on this page's menu, see element click intercepted.
Conclusion
StaleElementReferenceException means you kept a WebElement past the moment the page replaced its node. The Element: line in the message names the reference, and the stack trace points at the first use after the change. Find elements after the action that replaces them, keep By locators instead of elements, and wait for the new state. A retry only works if it looks the element up again, and on a page where you know what re-renders, finding the element at the right moment is enough.
Sources and further reading
- Selenium: Understanding common errors, StaleElementReferenceException, on the causes and the documented fixes.
- Selenium: Waiting strategies, for
WebDriverWaitand expected conditions. - Selenium: Working with select list elements, for the
Selectclass. - W3C WebDriver: Elements, which defines when an element reference is stale.
- Sauce Demo, where every example here was run.