Skip to content
QSWEQB
mediumConceptCodingMidSenior

Walk me through waiting in Selenium: implicit versus explicit, why you get StaleElementReferenceException, and how page objects fit in.

An implicit wait retries element location globally and nothing else; an explicit wait polls for a named condition and is the only tool that can wait for state. Stale references happen because a WebElement points at one DOM node that no longer exists, so the cure is re-locating rather than re-waiting.

5 min readUpdated 2026-07-26

What the interviewer is scoring

  • Whether the answer distinguishes waiting for an element from waiting for a state
  • Does the candidate know that mixing the two wait mechanisms is documented as unsafe rather than merely inelegant
  • That staleness is explained as a broken node reference, not as a timing problem to be slept away
  • Whether page objects are defended for a reason beyond "it avoids duplication"
  • Can they name a situation where the page-object pattern makes a suite worse

Answer

Two mechanisms that are not two settings of one thing

An implicit wait is a driver-level timeout, off by default, that changes the behaviour of element location alone. Once set, findElement polls the DOM until a match appears or the timeout expires, and it applies for the lifetime of that driver session to every lookup you make. That is its whole scope. It cannot wait for text to change, for a spinner to disappear, for a button to become enabled, or for an attribute to update, because none of those involve finding an element that was not there before.

An explicit wait is per-call and takes a condition. WebDriverWait polls that condition, by default every 500 milliseconds, until it returns something truthy or the timeout is reached. Because the condition is arbitrary, this is the only one of the two that can express what a test genuinely depends on, which is almost always a state rather than a presence.

The distinction has a practical edge in a case worth naming. With an implicit wait set, findElements on a locator that matches nothing does not return quickly — it waits out the full timeout before handing back an empty list. So a test that checks "no error rows are shown" pays ten seconds every time it passes, and a suite full of negative assertions gets slow for reasons that look like network flakiness.

Why mixing them is a bug, not a style choice

The Selenium documentation warns explicitly against combining implicit and explicit waits, and the reason is that the two poll loops nest. An explicit wait for elementToBeClickable calls element location internally, and each of those internal calls now has its own implicit timeout before it can fail and let the outer loop retry. The effective timeout stops being either number you configured, and the usual symptom is a test that intermittently takes far longer than its declared timeout before failing.

Pick one strategy per suite and enforce it. In practice that means leaving the implicit wait at zero and using explicit waits everywhere, because a codebase where waits are visible at the call site is one you can reason about.

// Global, invisible, and only ever waits for existence.
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));

// The element exists the instant the page renders, so the implicit wait
// above is satisfied immediately - and the assertion still races the
// asynchronous price update it depends on.
assertEquals("₹1,299", driver.findElement(By.id("total")).getText());

// Explicit, local, and waits for the condition the test is really about.
new WebDriverWait(driver, Duration.ofSeconds(10))
    .until(ExpectedConditions.textToBe(By.id("total"), "₹1,299"));

FluentWait is the same machinery with the knobs exposed: you set the timeout, the polling interval, and the exception types to swallow while polling. It earns its verbosity when you need to tolerate a specific transient failure, which is exactly the situation the next section describes.

Staleness is a broken reference, not a slow page

A WebElement is a handle to one particular node in the DOM at the moment you located it, held by the browser under an opaque identifier. If that node is removed from the document, or the framework re-renders the subtree and replaces it with a fresh node that looks identical, your handle refers to something that no longer exists and every method on it throws StaleElementReferenceException.

Two situations produce nearly all of them. The first is holding an element across a navigation or reload, where the entire document is new. The second, far more common on modern front ends, is a re-render triggered by data arriving: you locate a row, the list finishes loading and re-renders, and the click that follows lands on a reference to the old row. Nothing about this is a timing problem in the sense that waiting longer fixes it, so adding a sleep before the click makes the test slower and just as likely to fail — often more so, because the sleep gives the re-render time to happen after you located the element rather than before.

The fix is to shorten the lifetime of the reference. Locate and act in one breath, never store a WebElement in a field, and when a re-render is expected, either wait for the condition that means the re-render has finished before locating, or wrap the lookup so it re-locates after a stale failure. ExpectedConditions.refreshed exists for the second case, and it composes with another condition to re-run the whole lookup rather than retrying a dead handle.

// Re-locates from the By if the element goes stale mid-wait.
WebElement row = new WebDriverWait(driver, Duration.ofSeconds(10))
    .until(ExpectedConditions.refreshed(
        ExpectedConditions.elementToBeClickable(By.cssSelector("tr[data-row='51']"))));
row.click();

There is a related trap in PageFactory. Fields annotated with @FindBy are proxies that re-locate the element on each method call, which is quietly why they survive re-renders. Adding @CacheLookup to make them faster pins the reference after the first lookup and reintroduces staleness in a form that is hard to spot in review, because the annotation reads like a harmless optimisation.

What page objects buy, and where they stop paying

The pattern puts locators and interactions for a screen behind an interface expressed in the application's language, so a test says checkout.selectAddress("Home") and knows nothing about CSS. The benefit people quote is deduplication, but the stronger argument is that it gives the waiting a home: the page object is where "this screen is ready" is defined once, so every test inherits a correct wait instead of each author inventing one. It also localises churn, since a redesign changes one class rather than forty tests.

The limits are real and worth volunteering. Page objects grow into god classes when a screen is large, and a five-hundred-line object with forty methods is no easier to change than the duplication it replaced; component objects, composed into screens, scale better and match how the front end is actually built. The pattern also assumes pages, which a single-page application with nested reusable widgets does not really have. It tempts authors to put assertions inside the object, which quietly makes the object a test and hides what a failing test was checking. Chaining methods that return the next page object reads beautifully and encodes navigation order into the class graph, so one flow change ripples through unrelated screens. And none of it improves your selectors: a page object built on brittle XPath is a tidy wrapper around the same fragility, which is why stable test identifiers agreed with the developers do more for a suite than any amount of layering.

Wait for the state your assertion depends on, hold element references for as little time as possible, and treat layering as a way to centralise readiness rather than as an end in itself.

Likely follow-ups

  • What does findElements do when an implicit wait is set and nothing matches?
  • How would you write a retry that survives a component re-rendering mid-interaction?
  • Where do assertions live if the page object returns page objects?
  • Your application is a single page of nested components with no page boundaries. What replaces page objects?

Related questions

Further reading

seleniumwebdriverwaitsstale-elementpage-object