Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

The reliable fix is to stop reusing the old WebElement. Keep a locator, find the element again inside a bounded explicit wait, and wait for the state your next action actually needs. A StaleElementReferenceException means Selenium’s reference points to a DOM node that is no longer attached to the current document.

This guide shows a Java FluentWait pattern, the equivalent Python WebDriverWait approach, replacement-node handling, safe retries, diagnosis, and the failure modes that a longer timeout cannot solve.

What “stale” means

A Selenium WebElement is a handle to one particular DOM element. The handle becomes stale when navigation, refresh, a JavaScript framework update, or a frame/document change removes that node or replaces it with another node. The new element may look identical in the browser, but Selenium still holds the old reference.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Typical triggers include React/Vue/Angular re-renders, submitting a form that navigates, switching into a refreshed iframe, virtualized lists, and code that replaces innerHTML. The exception is not usually fixed by waiting longer while holding the same object; that object cannot become current again.

The core FluentWait pattern in Java

Java’s FluentWait repeatedly evaluates a condition until it returns a non-null/non-false value, an unignored exception is thrown, the timeout expires, or the wait is interrupted. You configure the maximum duration, polling interval, and only the transient exceptions you genuinely expect.

import java.time.Duration;
import org.openqa.selenium.By;
import org.openqa.selenium.StaleElementReferenceException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.FluentWait;
import org.openqa.selenium.support.ui.Wait;

Wait<WebDriver> wait = new FluentWait<>(driver)
    .withTimeout(Duration.ofSeconds(10))
    .pollingEvery(Duration.ofMillis(250))
    .ignoring(StaleElementReferenceException.class);

WebElement button = wait.until(d -> {
    WebElement current = d.findElement(By.cssSelector("button.submit"));
    return current.isDisplayed() && current.isEnabled() ? current : null;
});
button.click();

The important detail is d.findElement(...) inside the lambda. Every poll obtains a fresh reference. Returning null tells FluentWait to poll again; returning the element completes the wait.

Match the condition to the operation

  • For reading text, require visibility and then read from the returned element.
  • For typing, require visibility and enabled state, then clear and send keys.
  • For clicking, check displayed/enabled state, but recognize that the DOM can still change between the condition and click().
  • For a disappearance, wait for invisibility or staleness rather than presence.

If a click has an irreversible side effect, do not blindly retry the click after an uncertain failure. Re-find the element and retry the complete workflow only when duplicate execution is safe, or add an application-level idempotency check.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

A reusable Java helper

A small helper keeps locator lookup inside the retry boundary and makes the policy explicit:

import java.time.Duration;
import java.util.function.Function;
import org.openqa.selenium.By;
import org.openqa.selenium.StaleElementReferenceException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.FluentWait;

static WebElement waitForUsable(WebDriver driver, By locator) {
    return new FluentWait<WebDriver>(driver)
        .withTimeout(Duration.ofSeconds(10))
        .pollingEvery(Duration.ofMillis(250))
        .ignoring(StaleElementReferenceException.class)
        .until(d -> {
            WebElement e = d.findElement(locator);
            return e.isDisplayed() && e.isEnabled() ? e : null;
        });
}

WebElement submit = waitForUsable(driver, By.cssSelector("button.submit"));
submit.click();

Keep timeout and polling values appropriate to the application. A 10-second timeout and 250-millisecond polling interval are illustrative, not universal requirements.

Python: use WebDriverWait, not Java FluentWait methods

Selenium Python exposes WebDriverWait. Do not copy Java calls such as .withTimeout() or .pollingEvery(). The Python constructor accepts a driver, timeout, polling frequency, and ignored exceptions. Its documented polling default is 0.5 seconds, and NoSuchElementException is ignored by default.

from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.common.exceptions import StaleElementReferenceException

def usable(d):
    element = d.find_element(By.CSS_SELECTOR, "button.submit")
    return element if element.is_displayed() and element.is_enabled() else False

button = WebDriverWait(
    driver,
    timeout=10,
    poll_frequency=0.25,
    ignored_exceptions=(StaleElementReferenceException,),
).until(usable)
button.click()

Verify constructor details against the Selenium Python version installed in your project. As in Java, the locator lookup must happen during each poll.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

When a known node is being replaced

Sometimes the transition itself matters: an old row, button, or status element must detach before the replacement is queried. Selenium’s Python expected conditions include staleness_of, which remains false while the element is attached and becomes true after detachment.

from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

old_status = driver.find_element(By.ID, "status")
driver.find_element(By.ID, "refresh").click()

WebDriverWait(driver, 10).until(EC.staleness_of(old_status))
new_status = WebDriverWait(driver, 10).until(
    EC.visibility_of_element_located((By.ID, "status"))
)
print(new_status.text)

staleness_of only confirms that the old object detached. It does not locate or validate the replacement; always perform a fresh locator lookup afterward.

Fresh lookup versus cached elements

Fragile pattern

WebElement row = driver.findElement(By.cssSelector(".result"));
triggerUpdate.click();
wait.until(d -> row.isDisplayed()); // row may already be stale

Robust pattern

By rowLocator = By.cssSelector(".result");
triggerUpdate.click();
WebElement row = wait.until(d -> {
    WebElement current = d.findElement(rowLocator);
    return current.isDisplayed() ? current : null;
});

Store locators, not long-lived element objects, across operations that can redraw the page. Page-object fields that cache WebElement instances are especially likely to fail on dynamic screens.

Choosing the right wait

What is changing What to wait for Why
A known old node must detach staleness_of(oldElement) Confirms the old reference is no longer attached.
A replacement must be used Fresh locator plus visibility, enabled state, text, or another required condition Finds and validates the current node.
A click is needed Fresh locator plus displayed/enabled checks, with a safe retry policy Presence alone does not mean the action can succeed.
Text or an attribute is needed Fresh locator plus the expected value or non-empty state Synchronizes with application state rather than elapsed time.

Why fixed sleeps and broad ignores fail

Fixed sleeps

Thread.sleep or time.sleep pauses for a predetermined duration whether the page is ready or not. A short sleep creates a race; a long sleep slows every test. Condition-based waits stop as soon as the required state exists.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Ignoring too much

Ignoring StaleElementReferenceException is useful only when another poll can make progress. Do not ignore every exception: a bad selector, missing frame, invalid window, or application defect should surface instead of being hidden until timeout. Limit ignored exceptions to the transient failure you understand.

Waiting only for presence

An element can be in the DOM but hidden, disabled, covered by an overlay, or still receiving replacement content. Wait for the state required by the next line of test code.

Diagnosing a timeout

  1. Confirm the locator. Inspect the current DOM and verify that it still matches one intended element.
  2. Check navigation. A click may have loaded a different document; reacquire all post-navigation elements.
  3. Check frame and window context. Switch to the correct iframe or window before locating the element. A refreshed frame requires switching and locating again.
  4. Check application state. Make sure the expected request, animation, validation, or component update is actually triggered.
  5. Check overlays and enabled state. Presence does not prove that the control is interactable.
  6. Capture evidence. Record the URL, current frame/window, page source or screenshot, and the last condition result.
  7. Only then adjust the timeout. Increasing it cannot repair a wrong locator, wrong context, or state transition that never occurs.

Safe retry design for clicks and submissions

A stale exception can occur after the browser has accepted an action but before Selenium receives its response. Retrying a purchase, delete, form submission, or navigation can therefore duplicate side effects. For read-only actions, a bounded retry around lookup and action is usually simpler. For writes, prefer an idempotent endpoint, a unique request key, or a post-action assertion that proves whether the operation already happened.

Keep retries bounded by both time and attempts. Log each attempt and preserve the original exception when the policy is exhausted. A retry should make progress toward a known state, not repeatedly execute the same click without verification.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Implicit and explicit waits

Use one deliberate synchronization strategy and understand the settings already present in the project. Combining implicit and explicit waits can make timing behavior harder to reason about, especially when a condition performs several lookups. Keep explicit waits focused, avoid arbitrary sleeps, and document the timeout policy used by your test suite.

Version and API notes

The Python exception reference used for this guidance is labeled Selenium 4.49.0. Selenium APIs can change, so compile and run the examples against the exact Java or Python binding version in your build. Java’s FluentWait and Python’s WebDriverWait are related concepts, but their method names and defaults are not interchangeable.

Or skip the browser setup

If your goal is simply to capture a stable page image rather than drive an interactive Selenium workflow, ScreenshotNeo provides a single screenshot request. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be disabled. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server lets Claude, Cursor, or another MCP client call take_screenshot, get_page_info, and capture_pdf.

See the ScreenshotNeo documentation for request options. The cURL form is:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python:

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)

Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account to try it.

Frequently Asked Questions

Can I repair a stale element by calling refresh on the WebElement?

No. A stale object is a reference to a detached node. Keep its locator, wait for the required state, and locate a new element.

Should every FluentWait ignore StaleElementReferenceException?

No. Ignore it only around an operation where a later poll can obtain a fresh reference and retrying is safe. Broad or global ignores can hide real defects.

Why does my element become stale immediately after the wait succeeds?

The DOM can change between the condition and the next command. Re-find and retry the complete operation when that retry is safe, or synchronize on a stronger application state.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.