PRACTICAL GUIDE / Java Supplier lazy Selenium element lookup
Use Java Supplier without hiding stale Selenium elements
Use Java Supplier to defer Selenium element lookup, reproduce a stale reference after rerender, and decide when a plain By or explicit wait is clearer.
In this guide6 sections
What you will learn
- Why an element becomes stale after rerendering
- Reproduce the stale reference and the fresh lookup
- Confirm the stale reference is the real failure
- Choose the smallest abstraction that stays fresh
A page object stores a Save button during construction. The application rerenders the form, the new button looks identical, and the next click throws StaleElementReferenceException. The locator is still correct, but the stored element belongs to the old DOM.
A Java Supplier can defer that lookup until the action needs it, but used carelessly it only wraps the same stale reference in a lambda and makes the extra indirection harder to see.
Why an element becomes stale after rerendering
findElement does not return a live query. WebDriver returns an element reference associated with a particular node in the current document and browsing context. Selenium keeps that remote identity inside the WebElement so later calls such as click() and getText() can target the same node.
Modern UI frameworks often replace nodes instead of updating them in place. A button with id="save" can disappear and a new button with the same ID, text, and CSS classes can take its position. To a person, it is still the Save button. To WebDriver, the old reference points to a node that is no longer attached, so an action on it is stale.
Navigation destroys the old document and has the same effect. Switching windows or frames creates a different problem: the reference may belong to a valid element in another context, but it is not usable from the current one. Repeating findElement in the wrong frame does not repair that context error.
Supplier<T> has one relevant operation, get(). A lambda such as () -> driver.findElement(saveButton) delays the command until get() runs. Each invocation executes findElement again and can return the element currently matched by the locator.
This version is lazy but not fresh:
WebElement button = driver.findElement(By.id("save"));
Supplier<WebElement> wrong = () -> button;The lambda captures an element that was already resolved. Calling wrong.get() ten times returns the same reference ten times. The correct closure captures the recipe for finding the element:
By saveButton = By.id("save");
Supplier<WebElement> currentSaveButton =
() -> driver.findElement(saveButton);The Supplier adds no waiting, retrying, caching, or thread safety. It simply decides when code runs. Those omissions are useful because they keep policy separate, but only if the framework says so plainly.
Exceptions from findElement also pass straight through get(). The caller still needs to distinguish an element that is not ready from an invalid selector, dead session, or wrong context. Do not bury a broad catch inside the lambda. A name such as currentSaveButton also communicates the fresh-lookup behavior better than a field named saveButton, which readers may reasonably assume is a stored element.
Reproduce the stale reference and the fresh lookup
The following JUnit 5 test uses a self-contained data URL. Clicking Rerender synchronously replaces the Save button with a clone. The cached reference fails, while the Supplier locates the replacement.
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.StaleElementReferenceException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
class LazyElementLookupTest {
private WebDriver driver;
@BeforeEach
void openBrowser() {
driver = new ChromeDriver();
}
@AfterEach
void closeBrowser() {
if (driver != null) {
driver.quit();
}
}
@Test
void supplierFindsTheReplacementNode() {
String html = """
<!doctype html>
<html lang="en">
<body>
<button id="save"
onclick="this.dataset.state='clicked'">Save v1</button>
<button id="rerender" onclick="replaceSave()">Rerender</button>
<script>
function replaceSave() {
const oldButton = document.getElementById('save');
const newButton = oldButton.cloneNode(true);
newButton.textContent = 'Save v2';
oldButton.replaceWith(newButton);
}
</script>
</body>
</html>
""";
String encoded = Base64.getEncoder().encodeToString(
html.getBytes(StandardCharsets.UTF_8));
driver.get("data:text/html;base64," + encoded);
By saveLocator = By.id("save");
WebElement cachedSave = driver.findElement(saveLocator);
AtomicInteger lookupCount = new AtomicInteger();
Supplier<WebElement> currentSave = () -> {
lookupCount.incrementAndGet();
return driver.findElement(saveLocator);
};
driver.findElement(By.id("rerender")).click();
assertThrows(
StaleElementReferenceException.class,
cachedSave::click);
WebElement replacement = currentSave.get();
assertEquals("Save v2", replacement.getText());
replacement.click();
assertEquals(
"clicked",
currentSave.get().getAttribute("data-state"));
assertEquals(2, lookupCount.get());
}
}Run the class on its own:
mvn -Dtest=LazyElementLookupTest -DtrimStackTrace=false testThe example deliberately counts lookups. Lazy abstractions can hide remote calls, especially on Grid. A counter or command listener in framework tests makes that cost visible.
Notice that the test stores the first fresh result in replacement before reading and clicking it. Code such as currentSave.get().isDisplayed(); currentSave.get().click(); performs two separate lookups. The DOM can change between them, and the second lookup may target a different node from the one whose state was checked.
Confirm the stale reference is the real failure
Read the first exception, not the final retry result. A genuine stale-element stack trace appears when an operation uses a previously found WebElement. Record the locator, the action, and when the original lookup occurred. Without that timeline, a framework may blame a rerender that happened after the actual failure.
Reproduce with a focused command and no automatic test retry. Add a log at every Supplier invocation. If the failure happens without a new invocation, some code path is still using a cached element. If a new lookup succeeds and the subsequent action goes stale, the page is changing during the action window and Supplier alone cannot close that race.
Inspect the DOM transition in the browser. A framework may update the existing node's text, which does not necessarily invalidate the reference, or replace the node completely, which does. Break on subtree modifications in DevTools or log a stable application event around the rerender. A before-and-after screenshot cannot show node identity, so it is supporting evidence rather than proof.
Check the current window and frame next. A supplier that captures driver searches from whatever context that driver currently has. If another method switched into an iframe and never switched back, a fresh lookup can fail with NoSuchElementException even though the locator is valid in the top-level document. Restore the intended context before relocating.
Navigation deserves separate treatment too. If the test is unexpectedly on a login or error page, repeatedly finding By.id("save") is not resilience. Assert the page identity or URL at the boundary where navigation should complete, then look up the control.
Finally, compare the remote command log with source code. Every get() is normally a new find-element command. If one page action calls the supplier five times, the abstraction is adding traffic and widening the number of moments at which the DOM can change.
Choose the smallest abstraction that stays fresh
Storing By is often enough. A page method can call driver.findElement(saveLocator) immediately before the action. Reviewers see the lookup, there is no general-purpose functional wrapper, and Selenium's existing expected conditions work naturally with locators.
A Supplier is useful when code needs to pass deferred lookup behavior across a boundary. A component might accept Supplier<WebElement> because it must fetch the current root each time it performs an operation, while the page object retains ownership of the driver and locator. That is a concrete reason for the functional interface, not a style preference.
Keep readiness policy outside a bare Supplier. When the element can be late, an explicit wait can locate by By and return the ready element:
WebElement readySave = new org.openqa.selenium.support.ui.WebDriverWait(
driver, java.time.Duration.ofSeconds(5))
.until(org.openqa.selenium.support.ui.ExpectedConditions
.elementToBeClickable(By.id("save")));
readySave.click();This is clearer than teaching every Supplier to sleep or swallow exceptions. A DOM replacement can still occur after the condition succeeds and before click(), so investigate frequent races instead of adding unlimited retries.
If the framework retries a stale action, bound the retry and prove the action's outcome. A second click may hit a replacement button after the first click already reached the application. For non-idempotent actions such as payment, submission, or deletion, blind action retries can duplicate real work.
Pay for freshness only where it helps
Each fresh lookup is a WebDriver command. That cost is modest in a local browser and more visible on a remote Grid. Repeated get() calls in fluent assertion helpers can make a simple test chatty. Resolve once per logical check, and resolve again only after a known transition or wait boundary.
Freshness also changes identity semantics. If a list is reordered, the same locator may now match a different row. Relocating avoids a stale exception but can make the test act on the wrong entity. Include a stable record key in the locator and assert the row's identifying value before a destructive action.
The captured objects have lifetimes. A Supplier held in a static field can outlive the driver session it closed over. In parallel execution, sharing that Supplier can direct one test's action to another test's browser or to a session that has already quit. Keep deferred elements inside the page or component instance owned by one test.
Nested search contexts need the same care. A Supplier that captures a previously found container element can still become stale even if it finds the child lazily. Capture a locator chain or reacquire the container before searching within it.
Do not wrap every element in Supplier
Skip the abstraction for elements used once immediately after lookup. It adds a type and an invocation without changing timing. A local WebElement is fine when no navigation or rerender can occur between lookup and action.
Avoid Supplier as a blanket cure for poor synchronization. If the application is still loading, wait for the user-observable ready state. Repeated instant lookups merely fail closer to the action and can produce a different flaky exception.
Do not use fresh lookup when the test specifically needs to prove that the same DOM node persists. Replacing the node may itself be the defect, for example when focus, selection, or assistive-technology state must survive an update. A Supplier would silently follow the replacement and erase that evidence.
Most page objects need stable locators, explicit state transitions, and methods that look up elements near their use. Add Supplier<WebElement> only when deferred behavior crosses a real design boundary. The goal is not to prevent Selenium from ever saying "stale." The goal is to know when a replacement is expected, fetch the current node at that point, and keep unexpected replacements visible.
// FIELD DISPATCH
Get the QA Field Notes
Weekly QA battles, AI testing guides, and interview drills. Free on Substack.
// LIVE COURSE / THE TESTING ACADEMY
Playwright Automation Mastery
Go beyond Selenium. Master Playwright with JS/TS in 90 days.
From the instructor behind this guide.
Playwright jobs are growing 8x faster than Selenium. 90 days / 75+ live hrs / Tue-Thu-Sat 7 AM IST.
PRIMARY REFERENCES
Verify the details at the source
QABattle guides are practical explanations. Product behavior, standards, and APIs can change, so use these primary references for the canonical details.
- 01Official selenium.dev reference
selenium.dev
Primary documentation selected and verified for the claims in this guide.
- 02Official selenium.dev reference
selenium.dev
Primary documentation selected and verified for the claims in this guide.
- 03Official selenium.dev reference
selenium.dev
Primary documentation selected and verified for the claims in this guide.
- 04Official selenium.dev reference
selenium.dev
Primary documentation selected and verified for the claims in this guide.
FAQ / QUICK ANSWERS
Questions testers ask
What does Supplier WebElement do in Selenium?
A `Supplier<WebElement>` can defer `findElement` until `get()` is called. It provides a fresh lookup only when the lambda captures a locator and search context rather than an element that was already found.
Does a lazy element lookup prevent every stale element error?
No. The DOM can still replace an element between the fresh lookup and the next action, and a changed frame or page can make the locator invalid for the current context.
Is it better to store By or Supplier WebElement in a page object?
Storing `By` is simpler when methods can call `driver.findElement(locator)` directly. A Supplier earns its place when another component needs an executable lookup without taking ownership of the driver.
Can I combine Supplier with WebDriverWait?
Yes, a wait condition can call the supplier and return the element when the required state is present. In many cases, Selenium's existing expected conditions accept a `By` and express the same intent more clearly.
Is Supplier WebElement safe across parallel tests?
Not by itself. Thread safety and session ownership come from the captured driver and framework design; sharing a supplier that closes over one driver can send commands to the wrong session.
RELATED GUIDES
Continue the learning route
GUIDE 01
Selenium executeAsyncScript in Java
Selenium executeAsyncScript Java uses a final callback argument and the script timeout. Learn return conversion, error handling, and tested Java patterns.
GUIDE 02
Debug Java Classpath Conflicts in Selenium Frameworks
A practical guide to debug Java Selenium classpath conflicts, covering design, implementation, debugging, scale, measurable release gates, and senior interview scenarios.
GUIDE 03
Selenium Java Cookie and Browser Storage Testing
Master Selenium Java cookie storage testing with practical examples, architecture decisions, failure analysis, CI guidance, metrics, and scenario-led interview answers.
GUIDE 04
Selenium Java Grid Session Factory Architecture
A practical guide to Selenium Java grid session architecture, covering design, implementation, debugging, scale, measurable release gates, and senior interview scenarios.
GUIDE 05
Selenium Java Tutorial: Build a Maintainable Test Suite
Selenium Java tutorial for beginners covering Maven setup, WebDriver, waits, TestNG, JUnit, page objects, debugging, CI, and reliable UI tests.