Skip to content
SDETRole
SDETRole

Selenium

Selenium Waits Explained: Implicit, Explicit and Fluent (and Why Not to Mix Them)

How implicit, explicit and fluent waits really work in Selenium 4 with Java 21, why mixing them causes unpredictable timeouts, and how to handle stale elements.

By SDETRole Editorial Team Published 9 min read
Table of contents

Selenium does not wait for your application. When you call findElement or click, WebDriver sends the command to the browser immediately. If the element has not rendered yet because a request is still in flight or a framework is still hydrating, you get a NoSuchElementException or an ElementNotInteractableException, and a test that passed locally fails in CI.

Synchronisation is therefore the single most important skill in Selenium automation. Selenium gives you three tools for it: implicit waits, explicit waits and fluent waits. They are not interchangeable, and combining the wrong two creates timeouts that nobody can explain. This article covers how each works under the hood, with Selenium 4 and Java 21 examples, and ends with the stale element problem that catches people even when their waits are correct.

Why Thread.sleep is not a wait

Before the real options, the one to remove from your codebase:

driver.findElement(By.id("search")).sendKeys("laptop");
Thread.sleep(3000); // hope the results have loaded
driver.findElement(By.cssSelector("[data-test=result]")).click();

A fixed sleep is wrong in both directions. When the page is fast, you waste three seconds on every run. When the page is slow, three seconds is not enough and the test fails anyway. A wait should poll for a condition and continue the moment it is true.

Implicit waits

An implicit wait is a session-wide setting that tells the driver how long to keep trying to locate an element before throwing NoSuchElementException:

import java.time.Duration;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;

WebDriver driver = new ChromeDriver();
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(5));

Key facts about implicit waits:

  • The default is zero. Without setting it, findElement fails immediately if the element is absent.
  • It applies to element location only: findElement and findElements. It does not wait for an element to become visible, enabled or clickable.
  • It is enforced by the browser driver, not by your test code. Your client sends a find command and the driver keeps retrying until the element appears or the timeout elapses.
  • It applies for the lifetime of the session (or until you change it), to every find call.
  • findElements waits the full timeout when nothing matches, then returns an empty list. Checking that something is absent with findElements(...).isEmpty() therefore costs the full implicit wait every time.

The second point is the important one. An implicit wait gets you past “element not in the DOM yet”. It does nothing for “element is in the DOM but hidden behind a spinner” or “button exists but is disabled until the form validates”. Those are the more common synchronisation problems in modern applications.

Explicit waits

An explicit wait polls for a specific condition, for a specific element or state, with its own timeout. In Selenium 4, WebDriverWait takes a Duration:

import java.time.Duration;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;

var wait = new WebDriverWait(driver, Duration.ofSeconds(10));

WebElement results = wait.until(
    ExpectedConditions.visibilityOfElementLocated(By.id("search-results")));

until keeps evaluating the condition until it returns something that is neither null nor false, then returns that value. If the timeout expires first, it throws TimeoutException with a message describing the condition.

By default, WebDriverWait polls every 500 milliseconds and ignores NotFoundException (which includes NoSuchElementException) between polls, so a condition that looks up an element does not blow up while the element is still missing.

Useful ExpectedConditions

The ExpectedConditions class covers most needs:

Condition Waits until
presenceOfElementLocated(by) The element is in the DOM (may be hidden)
visibilityOfElementLocated(by) The element is in the DOM and displayed
elementToBeClickable(by) The element is visible and enabled
invisibilityOfElementLocated(by) The element is hidden or absent
textToBePresentInElementLocated(by, text) The element’s text contains the string
attributeToBe(by, name, value) An attribute has the given value
numberOfElementsToBe(by, n) Exactly n elements match
urlContains(fragment) The current URL contains the fragment
frameToBeAvailableAndSwitchToIt(by) The frame is available, then switches to it
alertIsPresent() A JavaScript alert is open
stalenessOf(element) The element is detached from the DOM

Conditions compose with ExpectedConditions.and(...), or(...) and not(...).

A typical interaction waits for the specific state you need, then acts:

wait.until(ExpectedConditions.invisibilityOfElementLocated(By.cssSelector(".loading-overlay")));
wait.until(ExpectedConditions.elementToBeClickable(By.id("place-order"))).click();
wait.until(ExpectedConditions.urlContains("/order/confirmation"));

Custom conditions with lambdas

Because until accepts a Function, you can write any condition inline. This is often clearer than combining several built-in conditions:

// Wait until the basket badge shows a specific count
String count = wait.until(d -> {
    String text = d.findElement(By.cssSelector("[data-test=basket-count]")).getText();
    return text.equals("3") ? text : null;
});

// Wait until a JavaScript-driven app reports it is idle
wait.until(d -> (Boolean) ((JavascriptExecutor) d)
    .executeScript("return window.appReady === true"));

Return null or false to keep polling; return anything else to stop.

Fluent waits

FluentWait is the general form that WebDriverWait is built on. (WebDriverWait extends FluentWait<WebDriver>.) Use it directly when you need control over the polling interval, the exceptions to ignore, the timeout message, or when you want to wait on something other than a driver:

import java.time.Duration;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.StaleElementReferenceException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.FluentWait;
import org.openqa.selenium.support.ui.Wait;

Wait<WebDriver> fluent = new FluentWait<>(driver)
    .withTimeout(Duration.ofSeconds(30))
    .pollingEvery(Duration.ofSeconds(1))
    .ignoring(NoSuchElementException.class)
    .ignoring(StaleElementReferenceException.class)
    .withMessage("Report export never reached the 'Ready' state");

WebElement status = fluent.until(d -> {
    WebElement el = d.findElement(By.id("export-status"));
    return el.getText().equals("Ready") ? el : null;
});

What ignoring() does is easy to misread. It does not suppress the exception from your test: it means “if the condition throws this exception during a poll, treat that poll as not yet satisfied and try again”. Once the timeout expires, you get a TimeoutException whose cause is the last ignored exception. ignoreAll(Collection) takes several types at once.

Ignore only the exceptions that represent a transient state. Ignoring WebDriverException broadly, for example, would also swallow a crashed browser and turn a clear error into a slow, confusing timeout.

You can also wait on non-driver objects. new FluentWait<>(apiClient) or new FluentWait<>(someElement) are both valid, which is handy for polling a backend until an asynchronous job completes.

Why mixing implicit and explicit waits is a bad idea

The Selenium documentation is direct about this: “Do not mix implicit and explicit waits. Doing so can cause unpredictable wait times.” It gives the example of a 10-second implicit wait combined with a 15-second explicit wait leading to a timeout after 20 seconds.

The cause is that the two waits operate at different layers:

  • The explicit wait runs in your test process. It calls the condition, sleeps for the polling interval, checks the elapsed time and calls again.
  • The implicit wait runs inside the browser driver. Every findElement call made by the condition can block for up to the implicit timeout before returning.

So with a 10-second implicit wait, a single poll of an explicit condition can take 10 seconds. The explicit wait only checks its deadline between polls. Trace it through for an element that never appears, with a 15-second explicit wait:

  1. 0s: first poll calls findElement, which blocks for 10s, then throws. Ignored.
  2. 10s: still under 15s, so poll again. findElement blocks for another 10s.
  3. 20s: the deadline has passed. TimeoutException.

The test failed at 20 seconds rather than 15. Change the page speed or the timeouts slightly and the result changes again, which is what “unpredictable” means in practice.

It gets worse for conditions that expect an element to be absent. invisibilityOfElementLocated succeeds when the element cannot be found, but with an implicit wait, every “cannot be found” takes the full implicit timeout. A spinner that disappears in 200 milliseconds can make each subsequent check cost five or ten seconds.

The fix is simple: set the implicit wait to zero and use explicit waits everywhere. If a legacy suite depends on an implicit wait and you cannot remove it at once, temporarily set it to zero around explicit waits, but treat that as a migration step, not a design.

// Recommended baseline for a new framework
driver.manage().timeouts().implicitlyWait(Duration.ZERO);
driver.manage().timeouts().pageLoadTimeout(Duration.ofSeconds(30));

Other timeouts worth knowing

Two more session timeouts sometimes get confused with waits:

  • Page load timeout (pageLoadTimeout) limits how long driver.get() and navigation wait for the page load strategy to complete. The W3C default is 300 seconds, which is far too long for a test; most frameworks set something much shorter.
  • Script timeout (scriptTimeout) limits how long executeAsyncScript waits for its callback. The default is 30 seconds.

Neither one waits for your application’s dynamic content. A page can finish loading while XHR requests are still populating it.

StaleElementReferenceException

A WebElement is a reference to a specific node in the DOM. If that node is removed or replaced, for example when a framework re-renders a list, navigates, or swaps a button for a new one, the reference becomes stale. Any call on it throws StaleElementReferenceException, even if an identical-looking element is now on the page.

WebElement saveButton = driver.findElement(By.id("save"));
saveButton.click();          // triggers a re-render
saveButton.getText();        // StaleElementReferenceException

This is not a timing problem, so a longer wait on the same reference never fixes it. Approaches that do:

Re-locate instead of holding references

Store By locators in page objects, not WebElement fields, and find the element at the point of use. This removes most stale element errors on its own.

private final By saveButton = By.id("save");

public void save() {
    wait.until(ExpectedConditions.elementToBeClickable(saveButton)).click();
}

Wait for the old element to go stale

When an action replaces content, wait for the old node to detach before reading the new one. This is also a reliable way to know a refresh has actually happened:

WebElement oldTable = driver.findElement(By.id("results"));
driver.findElement(By.id("apply-filter")).click();
wait.until(ExpectedConditions.stalenessOf(oldTable));
WebElement newTable = wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("results")));

Use refreshed() for elements that re-render repeatedly

ExpectedConditions.refreshed(condition) wraps a condition and treats a stale element as “not yet”, re-evaluating it on the next poll:

wait.until(ExpectedConditions.refreshed(
    ExpectedConditions.elementToBeClickable(By.cssSelector("[data-test=live-row] button"))));

Note that refreshed only helps if the inner condition re-locates the element by By. Wrapping a condition built from a stored WebElement re-checks the same stale reference.

A practical waiting strategy

Putting it together, a robust Selenium 4 framework usually looks like this:

  1. Implicit wait set to zero.
  2. A single WebDriverWait per page object or test, with a timeout from configuration.
  3. Page objects store By locators and resolve them through helper methods that wait for the right state (visible, clickable, text present).
  4. Navigation and refreshes are followed by a wait for something specific on the new page, or for the old content to become stale.
  5. FluentWait for unusual polling: long-running jobs, custom intervals, or non-driver objects.
  6. No Thread.sleep outside of genuinely time-based behaviour under test.

You can practise these patterns in the Selenium Lab, and the Selenium interview questions cover the wait questions interviewers ask most often.

Summary

  • Implicit waits are session-wide, driver-side and only cover locating elements. The default is zero.
  • Explicit waits (WebDriverWait with ExpectedConditions or a lambda) poll for a specific condition and are the right default.
  • FluentWait gives full control over timeout, polling interval, ignored exceptions and message.
  • Mixing implicit and explicit waits makes each poll block on the implicit timeout, so overall timeouts become unpredictable. The Selenium docs warn against it; set implicit waits to zero.
  • StaleElementReferenceException means the node was replaced. Re-locate by By, wait for stalenessOf after re-renders, and use refreshed() sparingly.
Share LinkedInXReddit

#selenium#java#waits#synchronisation#webdriver

SDETRole Editorial Team

Practising SDETs and QA engineers who write, test and review every SDETRole article and lab. Code samples are run against current tool versions before publishing.

Type to search across SDETRole.

Full-text search by Pagefind · runs in your browser Open search page →