skip to content

Your custom Selenium wait condition reuses a WebElement found before the wait and dies with StaleElementReferenceException. Why?

level: seniorimportance: should knowfreq 47%

answer

  1. The lambda captured something it should not have
  2. Only one exception type is ignored by default
  3. Staleness sits outside NotFoundException
  4. Locators cross the boundary, handles do not
  5. Re-find inside the condition every poll

basics

~20 s

The lambda captured an element handle taken before the page re-rendered, so a later poll touches a detached node. WebDriverWait ignores only NotFoundException by default, and staleness is not one, so it aborts the wait.

solid answer

~40 s

Your lambda **closes over** a `WebElement` found before the change you are waiting for. When the page re-renders — changing a quantity on a plant-nursery order form replaces the subtotal cell rather than editing it — that handle points at a detached node and every read from it throws `StaleElementReferenceException`. `WebDriverWait` adds only `NotFoundException` to its ignore list, and `StaleElementReferenceException` extends `WebDriverException` directly, so it is not ignored: it propagates out of `until` and ends the wait instead of triggering another poll. The fix is to let only locators cross the lambda boundary and find the element inside the condition body, so every poll resolves whatever node exists at that moment. That also makes the not-yet-rendered case free, since `findElement` throws `NoSuchElementException`, which the wait does ignore.

code

java · 7 lines
java
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));

String subtotal = wait.until(d -> {
  WebElement cell = d.findElement(By.cssSelector("#order-subtotal"));
  String text = cell.getText();
  return text.startsWith("$") ? text : null;
});

go deeper

for a junior

Take the habit early: pass locators into the condition, never element handles. Find the element inside the lambda so each poll works with whatever is on the page right now.

for a middle

Explain the mechanics: a WebElement is a handle to one node, a re-render replaces that node, and the wait only ignores NotFoundException, so staleness escapes rather than causing another poll.

for a senior

Diagnose it from a stack trace. Show why the failure surfaces inside until rather than as a timeout, and why the same test passes on a faster machine and fails on a loaded one.

for a principal

Treat it as a codebase pattern, not one bug. Decide how shared helpers and page objects express waits so element handles never outlive the poll that produced them.

## Why the exception escapes the loop instead of being retried `WebDriverWait` is not a blanket retry. Its constructor adds exactly one entry to the ignore list: `NotFoundException`. While the condition is being applied, anything that is an instance of an ignored type is swallowed, remembered as the last exception, and the loop goes round again. Anything else is **propagated immediately**, which ends the wait then and there. The hierarchy decides the outcome, and it is worth memorising: | Exception | Extends | Inside a `WebDriverWait` condition | |---|---|---| | `NoSuchElementException` | `NotFoundException` | ignored, keeps polling | | `NoSuchFrameException` | `NotFoundException` | ignored, keeps polling | | `StaleElementReferenceException` | `WebDriverException` | **propagates, wait dies** | | `ElementClickInterceptedException` | `InvalidElementStateException` | propagates, wait dies | | `JavascriptException` | `WebDriverException` | propagates, wait dies | So the asymmetry that catches people is this: **a missing element is retried, a stale element is not.** `StaleElementReferenceException` extends `WebDriverException` directly, not `NotFoundException`, so it is outside the default ignore list and travels straight out of `until`. ## What makes the reference stale on this form A `WebElement` is a handle to a specific node in the browser's current DOM, identified by an element reference the remote end issued when you found it. The handle stops resolving the moment that node is detached. On a **plant-nursery order form** that happens constantly: - Changing the **quantity** on a line makes the framework re-render the subtotal cell, replacing the node rather than editing its text. - Choosing a different **delivery week** re-renders the whole price panel, so every element inside it is replaced. - Removing a plant from the order rebuilds the **order lines table**, invalidating every row handle taken earlier. - Any client-side navigation replaces the document, after which **every** reference from the previous document is stale. The dangerous version is a condition written like this: ```java WebElement subtotal = driver.findElement(By.cssSelector("#order-subtotal")); driver.findElement(By.id("quantity")).sendKeys("3"); String text = new WebDriverWait(driver, Duration.ofSeconds(10)) .until(d -> subtotal.getText().startsWith("$") ? subtotal.getText() : null); ``` The lambda **closes over** `subtotal`, a handle taken before the change that causes the re-render. The first poll that lands after the re-render throws `StaleElementReferenceException`, the wait does not ignore it, and the test fails inside `until` — often with a message that says nothing about waiting at all. ## Re-resolve inside the condition The condition is handed the driver as its argument for exactly this reason: it is meant to resolve the page fresh on each evaluation. The rewrite is mechanical: 1. Take **no element handles from outside** the lambda; close over locators and expected values only. 2. **Find the element inside the body**, so every poll gets a handle to whatever node exists right now. 3. Read from that fresh handle, and return `null` while the read does not yet show what you want. 4. Return the value — or the freshly found element — so the caller uses the handle the last poll produced, not one from before. Step 4 matters as much as step 2. If `until` returns a `WebElement`, that handle is only as fresh as the moment the last poll found it; acting on it immediately is fine, but storing it in a page-object field and using it after the next re-render puts you straight back in the same failure. Written that way, the earlier example becomes safe, and it also gains something for free: `findElement` inside the body throws `NoSuchElementException`, which **is** a `NotFoundException` and therefore is ignored, so a condition that re-finds handles the not-yet-rendered case without any extra code. ## Interview framing - Name the hierarchy fact — `StaleElementReferenceException` extends `WebDriverException`, not `NotFoundException` — rather than saying vaguely that Selenium "does not retry it". - Explain that the closure is the bug: the lambda captured a handle from before the state change it is waiting on. - Point out the irony that the wait was added to make the test robust, and the captured handle made it strictly more fragile. - Say what the fix is in one line: **locators cross the lambda boundary, element handles do not.** - Note that a wait returning an element hands back a handle whose freshness expires as soon as the page re-renders again. - Separate the two questions a reviewer will ask: whether the condition observes the right signal, and whether it observes it through a handle that can still resolve. A perfectly chosen signal read through a captured element fails just as reliably as a badly chosen one, and the stack trace looks nothing like a synchronisation problem, which is what makes this bug expensive to find in a large suite.

  • Why does the same condition sometimes pass and sometimes fail?
    It is a race between the polling loop and the re-render. If the wait's first evaluation lands before the framework replaces the node, the captured handle still resolves and the condition can even succeed. If the re-render happens first, that same handle is already detached. The test's outcome then depends on timing rather than on the behaviour it claims to check.
  • Is it safe to return a WebElement from the condition and use it afterwards?
    For the immediate next step, yes: the handle is as fresh as the last poll that produced it. It is not safe to store. Anything that re-renders the region invalidates it again, so a returned element belongs in a local variable used straight away, never in a field that later steps reuse.
  • What tells you a failure is a captured-handle bug rather than a genuinely missing element?
    The exception type. A missing element inside a condition raises `NoSuchElementException`, which the wait ignores, so you would see `TimeoutException` at the end instead. Seeing `StaleElementReferenceException` thrown from inside `until` means a handle existed and then stopped resolving, which only happens when something took that handle earlier.

saying these in an interview costs you the question

  • Believes WebDriverWait retries every WebDriverException by default
  • Captures an element outside the lambda and reuses it each poll
  • Thinks a stale handle silently re-resolves against the new node
  • Confuses a missing element with a detached one
  • Stores an element returned by until in a long-lived field