ScreenshotNeo

BlogHow-to

How to Handle Stale Element Exceptions in Selenium with Java

Learn why Selenium Java throws StaleElementReferenceException and how to recover with fresh locators, explicit waits, and safe bounded retries.

By the ScreenshotNeo team4 October 20268 min read

StaleElementReferenceException means Selenium is holding a reference to an element that is no longer attached to the current DOM. Save a stable By locator, wait for the page state you need, and locate the element again immediately before using it. When an action should replace an element, wait for the old element to become stale and then find the replacement.

A stale reference does not necessarily mean the selector is wrong. The same selector can match a newly created DOM node, but Selenium treats that node as a different element. Selenium defines the exception as a reference to an element that no longer appears in the page DOM (StaleElementReferenceException Java API).

1. What makes a Selenium element stale?

A WebElement is a reference to a particular DOM element, not a reusable query. Selenium checks whether that reference is still fresh when you call a method on it. If the page removes or replaces the node, later calls through the old reference fail. A new node that looks identical is still a different object (WebElement Java API).

Common causes include:

  • A page refresh or navigation.
  • A framework redraw after a state change, search, sort, pagination, or form submission.
  • A parent element being replaced, which also detaches its children.
  • Switching to a different window, tab, or frame and then using a reference from the prior context.
  • Finding an element before an asynchronous update completes, then using it after the update.

Selenium’s troubleshooting guide recommends checking page expectations, locator correctness, DOM updates, and wait strategy when diagnosing this class of error (Understanding Common Errors).

2. Set up a runnable Java example

The examples use Selenium 4, Java 11 or later, and Maven. Add Selenium’s Java binding as a dependency, then run a test or application with a browser driver configured for your environment. The code assumes driver is an initialized WebDriver.

<dependency>
  <groupId>org.seleniumhq.selenium</groupId>
  <artifactId>selenium-java</artifactId>
  <version>4.27.0</version>
</dependency>

Use the current Selenium version approved by your project if it differs from the illustrative dependency version above. The import set used in the examples is:

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.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;

3. Prefer a locator and fetch the current element at use time

For most interactions, keep the locator and pass it to an explicit wait. The locator-based condition performs a fresh lookup as it evaluates, and returns the element that is currently visible and enabled:

By saveButton = By.cssSelector("button.save");
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));

WebElement currentSaveButton = wait.until(
    ExpectedConditions.elementToBeClickable(saveButton));
currentSaveButton.click();

This is usually clearer than storing a WebElement for a long time and hoping the page does not redraw. A clickability check does not lock the DOM: a redraw can still occur after the condition succeeds and before the click command executes. If that race is plausible, wait for the relevant transition or use a narrowly bounded retry only if repeating the action is safe.

4. Wait for the old element to detach, then locate its replacement

Use stalenessOf when an interaction is expected to remove or replace a known element. Then wait for the replacement’s actual state:

By resultsLocator = By.id("results");
By refreshLocator = By.id("refresh-results");

WebElement oldResults = driver.findElement(resultsLocator);
driver.findElement(refreshLocator).click();

WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
wait.until(ExpectedConditions.stalenessOf(oldResults));
WebElement updatedResults = wait.until(
    ExpectedConditions.visibilityOfElementLocated(resultsLocator));

System.out.println(updatedResults.getText());

The first wait confirms detachment. The second confirms that an element matching the locator is visible. If the UI updates the contents in place without replacing the node, stalenessOf will not become true; wait instead for a changed text, attribute, or other meaningful condition.

5. Make a condition tolerant of a redraw

Sometimes a condition finds an element and then the application redraws it before the condition can finish checking. Wrap the condition with refreshed to allow it to be reevaluated when this happens:

By resultLocator = By.cssSelector(".result");
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));

WebElement result = wait.until(ExpectedConditions.refreshed(
    ExpectedConditions.visibilityOfElementLocated(resultLocator)));

System.out.println(result.getText());

refreshed is for a condition interrupted by an update or redraw; it is not a reason to ignore a page that never reaches the expected state. The Java API documents stalenessOf, refreshed, and locator-based conditions in ExpectedConditions.

6. Use a bounded retry only when repeating the action is safe

A retry can be useful for a known, brief redraw race. Re-locate inside each attempt, catch only StaleElementReferenceException, and set a small retry limit. The sample retries reading text, which does not change application state:

static String readTextWithRetry(WebDriver driver, By locator, int maxAttempts) {
    for (int attempt = 1; attempt <= maxAttempts; attempt++) {
        try {
            return driver.findElement(locator).getText();
        } catch (StaleElementReferenceException stale) {
            if (attempt == maxAttempts) {
                throw stale;
            }
        }
    }
    throw new IllegalStateException("No attempt was made");
}

For an action, apply the same structure only after deciding that a repeated command cannot cause a duplicate purchase, submission, deletion, or other side effect. A click may have succeeded even if a later read or navigation check failed. Prefer waiting for a post-action state (such as a confirmation or changed URL) over clicking again blindly.

7. Choose the right recovery pattern

Pattern Use it when What it establishes
Locate at use time Routine interaction on a dynamic page A current element is found from a stable locator
stalenessOf(oldElement) An action is expected to detach a known node The old reference is no longer attached
refreshed(condition) A redraw may interrupt a condition evaluation The condition can be evaluated again around a redraw
Bounded retry A known transient race remains and repetition is safe A limited number of fresh lookups and attempts

Re-locating incurs another WebDriver command, which can add latency when the browser is remote. That cost is usually preferable to using an invalid reference; avoid unnecessary repeated lookups in tight loops, and keep the locator stable and specific.

8. Diagnose the browsing context and update

  1. Identify the exact command that threw the exception. A failure on getText() after a successful click has different retry implications from a failure on the click itself.
  2. Confirm the active URL, window handle, and frame. Switch to the intended window or frame before finding the element.
  3. Determine whether the page navigated, replaced a node, redrew a parent, or only changed data in place.
  4. Check that the locator identifies the intended current element and does not match several unrelated nodes.
  5. Replace fixed sleeps with a condition tied to the update: visibility, clickability, old-node staleness, changed text, or another observable state.
  6. Keep the original exception and relevant locator/context details in failure logs. Do not turn an unrelated WebDriver failure into a silent retry.

9. Common errors and fixes

Symptom Likely cause Fix
Stale immediately after refresh or navigation The element belongs to the old document Wait for the new page state, then locate again.
Stale after clicking a filter or sorting control The app replaced the result list or a parent node Wait for old-list staleness if replacement is expected, then wait for the new list by locator.
Stale despite a long Thread.sleep Elapsed time does not prove the expected DOM transition occurred Use an explicit wait for the target state. A fixed delay can also waste time on fast runs.
Retry still throws stale The page keeps redrawing, the wrong context is active, or the state never stabilizes Check the update trigger and frame/window, then wait for a meaningful stable state. Keep retries bounded.
Element not found after waiting for staleness The replacement has not appeared, the locator changed, or the node updated in place Verify the expected behavior and locator; use a content/state condition if no replacement is created.
Duplicate submission after retry The first action succeeded but a later operation failed Do not retry non-idempotent actions blindly; detect completion before any repeat.
Stale after switching tabs or frames The element reference was obtained in another browsing context Switch into the correct context and find a fresh element there.

10. Reliability, performance, and cost notes

  • Reliability: waits should express the application state the test needs. A longer timeout cannot fix a wrong locator, wrong frame, or incorrect assumption about whether the node is replaced.
  • Performance: each lookup or wait poll is a WebDriver command. On a remote grid, excess polling and redundant lookups can increase test duration. Use one condition for the desired state and avoid repeated polling loops around arbitrary sleeps.
  • Retries: cap attempts and let the test fail with context when the condition does not stabilize. Retrying every WebDriver exception can conceal real defects.
  • Wait configuration: the examples use explicit waits. Mixing implicit and explicit waits can produce confusing timing; review Selenium’s waits guidance and choose a consistent strategy.
  • Cost: Selenium itself has no per-screenshot fee implied by these patterns, but browser execution consumes test-runner or grid time. Reduce avoidable waits while preserving checks for the actual UI state.

11. Or skip the browser setup

If your goal is to capture a page image or PDF rather than interact with it as a browser test, ScreenshotNeo provides a website screenshot API and MCP server. One GET request captures a URL. See the API documentation for parameters.

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://stripe.com \
  -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://stripe.com'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);

ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before capture; each cleanup 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 offers take_screenshot, get_page_info, and capture_pdf to AI agents and MCP clients. The Free plan includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000. Create a free account and get 1,000 screenshots a month with no card.

12. FAQ

Does stale mean my CSS selector is invalid?

No. The selector may still identify the intended element after the application creates a replacement node. The stored element reference is what became invalid.

Can I reuse a stale WebElement after catching the exception?

No. Find a fresh element from its locator. Catching the exception does not revive the old reference.

Should I always use refreshed?

No. Use it when redraw can interrupt evaluation of a condition. For an expected replacement, an explicit wait for staleness followed by the new state is more descriptive.

Is elementToBeClickable a guarantee that click will work?

No. It checks visibility and enabled state when evaluated. The page can change before the next WebDriver command.