Random "Element is no longer attached to the DOM" StaleElementReferenceException

Solution 1:

Yes, if you're having problems with StaleElementReferenceExceptions it's because there is a race condition. Consider the following scenario:

WebElement element = driver.findElement(By.id("foo"));
// DOM changes - page is refreshed, or element is removed and re-added
element.click();

Now at the point where you're clicking the element, the element reference is no longer valid. It's close to impossible for WebDriver to make a good guess about all the cases where this might happen - so it throws up its hands and gives control to you, who as the test/app author should know exactly what may or may not happen. What you want to do is explicitly wait until the DOM is in a state where you know things won't change. For example, using a WebDriverWait to wait for a specific element to exist:

// times out after 5 seconds
WebDriverWait wait = new WebDriverWait(driver, 5);
    
// while the following loop runs, the DOM changes - 
// page is refreshed, or element is removed and re-added
wait.until(presenceOfElementLocated(By.id("container-element")));        

// now we're good - let's click the element
driver.findElement(By.id("foo")).click();

The presenceOfElementLocated() method would look something like this:

private static Function<WebDriver,WebElement> presenceOfElementLocated(final By locator) {
    return new Function<WebDriver, WebElement>() {
        @Override
        public WebElement apply(WebDriver driver) {
            return driver.findElement(locator);
        }
    };
}

You're quite right about the current Chrome driver being quite unstable, and you'll be happy to hear that the Selenium trunk has a rewritten Chrome driver, where most of the implementation was done by the Chromium developers as part of their tree.

PS. Alternatively, instead of waiting explicitly like in the example above, you can enable implicit waits - this way WebDriver will always loop up until the specified timeout waiting for the element to become present:

driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS)

In my experience though, explicitly waiting is always more reliable.

Solution 2:

I have been able to use a method like this with some success:

WebElement getStaleElemById(String id) {
    try {
        return driver.findElement(By.id(id));
    } catch (StaleElementReferenceException e) {
        System.out.println("Attempting to recover from StaleElementReferenceException ...");
        return getStaleElemById(id);
    }
}

Yes, it just keeps polling the element until it's no longer considered stale (fresh?). Doesn't really get to the root of the problem, but I've found that the WebDriver can be rather picky about throwing this exception -- sometimes I get it, and sometimes I don't. Or it could be that the DOM really is changing.

So I don't quite agree with the answer above that this necessarily indicates a poorly-written test. I've got it on fresh pages which I have not interacted with in any way. I think there is some flakiness in either how the DOM is represented, or in what WebDriver considers to be stale.

Solution 3:

I get this error sometimes when AJAX updates are midway. Capybara appears to be pretty smart about waiting for DOM changes (see Why wait_until was removed from Capybara ), but the default wait time of 2 seconds was simply not enough in my case. Changed in _spec_helper.rb_ with e.g.

Capybara.default_max_wait_time = 5

Solution 4:

I was facing the same problem today and made up a wrapper class, which checks before every method if the element reference is still valid. My solution to retrive the element is pretty simple so i thought i'd just share it.

private void setElementLocator()
{
    this.locatorVariable = "selenium_" + DateTimeMethods.GetTime().ToString();
    ((IJavaScriptExecutor)this.driver).ExecuteScript(locatorVariable + " = arguments[0];", this.element);
}

private void RetrieveElement()
{
    this.element = (IWebElement)((IJavaScriptExecutor)this.driver).ExecuteScript("return " + locatorVariable);
}

You see i "locate" or rather save the element in a global js variable and retrieve the element if needed. If the page gets reloaded this reference will not work anymore. But as long as only changes are made to doom the reference stays. And that should do the job in most cases.

Also it avoids re-searching the element.

John