In Selenium, why does a loop over a cached List of WebElement rows start failing part-way through?
answer
- The list was captured at one instant
- Ask what the loop body does
- Handles, not locators, sit in that list
- Every remaining entry dies together
- Shrink the gap between find and use
basics
~20 sThe list is a snapshot of references captured at one instant, not a live view. If the loop body changes the page, the rows it replaces are detached and every remaining handle in the list is dead.
solid answer
~40 s`findElements` hands back a list of handles to the nodes that matched at that moment. Nothing keeps that list in step with the page. In a wine-cellar grid, clicking a per-row control makes the application re-render the table body, which detaches the `<tr>` nodes the list still names — usually all of them, not just the one you acted on — so iteration blows up on the second pass. The fix is to stop carrying handles across the action: either read everything you need from the grid first and act afterwards, or re-find the row inside each iteration immediately before using it. Both shrink the window between the find and the use to nearly nothing, which is where staleness lives.
code
java · 10 linesvoid decrementEveryBottle(WebDriver driver) {
List<String> labels = new ArrayList<>();
for (WebElement row : driver.findElements(By.cssSelector("#cellar-grid tbody tr"))) {
labels.add(row.findElement(By.cssSelector(".bottle-label")).getText());
}
for (String label : labels) {
By rowBy = By.cssSelector("#cellar-grid tr[data-label='" + label + "']");
driver.findElement(rowBy).findElement(By.cssSelector(".decrement")).click();
}
}go deeper
Know that a list returned by a find is a snapshot of handles taken once, and that acting on the page can invalidate every entry in it at the same moment.
Be ready to explain the mechanics: the snapshot binds one handle per matched node, a re-render detaches those nodes wholesale, and the failure therefore lands on a later iteration.
Show the restructuring and its tradeoff — collect data in one pass and act in a second, or re-find per iteration — and justify the extra round trips against a remote browser.
Own the pattern across the suite: make holding element handles across an action a reviewable defect, so this failure mode stops being rediscovered test by test.
## What the saved list really holds `driver.findElements(By.cssSelector("#cellar-grid tbody tr"))` returns a `List<WebElement>`: one handle per node that matched **at the instant the command ran**. It is a snapshot. The list is an ordinary Java collection of references, and nothing in Selenium keeps it in step with the document afterwards — it does not grow when rows appear, shrink when they are removed, or re-resolve when nodes are replaced. Each entry is bound to one node, exactly as a single `findElement` result is. That is fine as long as the page holds still. The trouble starts when the loop body is the thing that moves it. ## Why the loop dies part-way through A typical wine-cellar case: the test collects every row, then walks the list clicking each row's decrement control. The first click works. The application then re-renders the grid body to show the new quantity — and most front-ends re-render the **whole** body rather than patching one cell. Every `<tr>` in the snapshot is now detached, so the second iteration's handle is stale before it is touched. The details that make this confusing in a failure report: - The failure lands on **iteration two**, so the stack trace points at a row that has nothing wrong with it. - The grid on screen looks perfectly healthy — there are rows, they are visible, the data is correct. Only the references are dead. - The blast radius is the re-render's scope, not the user action's scope. Acting on one row can invalidate all of them. - Reversing the iteration order does not help. Direction is irrelevant when every entry is detached simultaneously. - If the loop body navigates rather than re-renders, the whole list dies for the other reason, and re-finding on the page you have landed on will not produce the rows at all. ## The window between find and use Every stale-reference failure has a measurable window: the elapsed commands between the find that produced the handle and the command that used it. In a well-written test that window is one line. In the loop above it is the entire body of the previous iteration, including a click that was designed to change the page. So the design rule is not "avoid `findElements`". It is: **never let a handle cross an action that can change the page.** Everything below is a way of honouring that. ## Two restructurings that hold up 1. **Read first, act second.** Walk the snapshot once to extract the plain data you need — labels, vintages, quantities — into ordinary strings. Strings do not go stale. Then loop over the strings, and for each one re-find its row and act. Each handle now lives for a single command. 2. **Re-find inside the iteration.** Drive the loop by an index or by a stable per-row value, and take a fresh `findElement` immediately before each use. Nothing is held across the action, so nothing can be detached while you hold it. | | Read first, act second | Re-find inside the iteration | |---|---|---| | Handles held across an action | none | none | | Extra lookups | one pass, then one per action | one per iteration | | Copes with rows vanishing | yes, when driven by row identifiers | yes, when the identifier still matches | | Suits a test that | mostly needs the data | acts on most of the rows | Both share the same idea: the loop's memory becomes data, and the handles become disposable. Which one to pick depends on whether the action changes the set of rows. If decrementing can remove a sold-out bottle from the grid, an index-driven loop over a count captured up front will drift; re-finding by a per-row identifier is the safer of the two. ## What it costs, and why it is still right Re-finding is not free. Each `findElement` is another command to the browser, and against a remote Grid that is another round trip. A loop over two hundred cellar rows that re-finds each one is measurably slower than one that reuses a snapshot. Weigh that honestly, and it still comes out in favour of re-finding: - The snapshot version is not faster in the case that matters — it fails. - Extracting the data in one pass and acting in a second pass costs one extra pass, not one extra command per use. - A test that reads the whole grid but only acts on two rows should collect data for all of them and handles for neither. ## When the data was all you needed A good share of these loops never needed to act at all. If the test is asserting that the grid shows six Burgundy bottles with the right vintages, collect the text in one pass and assert on the collected values. The handles have served their purpose the moment their text is read, and a later re-render cannot invalidate a `List<String>`. The general form: **hold data across page changes, never handles.** Handles are valid for one command against one document; anything you need to survive longer than that should have been copied out of the browser before the page moved.
- Why does reversing the loop so the last row is handled first not fix this?Because the re-render detaches every row node at once, not progressively from the top. Direction only helps when removals shift positions in a live collection, and this list is a snapshot of references rather than a live view. After the first action all remaining handles are stale whichever end you start from.
- Re-finding a row on every iteration adds a command per row. When is that cost worth arguing about?When the grid is large and the driver is remote, so each lookup is a round trip. Even then, prefer collecting the data in a single pass and acting in a second, rather than reusing handles. That costs one extra pass rather than one extra command per use, and it is the version that actually passes.
- How would you structure the loop if acting on a row can remove it from the grid entirely?Do not drive the loop by an index into a count captured up front, because removals shift every later position. Iterate over stable per-row identifiers collected in the first pass, re-find each row by its identifier, and treat a missing row as an outcome to assert on rather than an error.
saying these in an interview costs you the question
- Believes a list of elements updates itself as the page changes
- Thinks only the row acted on becomes stale
- Collects every row handle first, then acts on each
- Assumes iterating backwards avoids the problem
- Reads the stack trace as blaming the second row