Selenium Waits & Expected Conditions
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
Most flaky Selenium tests are synchronisation bugs: the script sends the next command before the application is ready for
it. This page explains the three ways Selenium waits (implicit, explicit and fluent), why you must not mix the first two,
the Expected Conditions that describe common things worth waiting for, and the support features that help you observe and
protect a driver (listeners that fire on every command, and ThreadGuard). It closes with the troubleshooting pages of the
official documentation: the common errors, the "unable to locate driver" error, and logging. It was written against
Selenium 4.51.0 (released 9 October 2026) and checked on 25 October 2026. The Java (JUnit 5.14.4, Java 17) and Python
(pytest) examples were run against the Bookshelf
application with headless Chrome 155 on macOS; Bookshelf was started with BOOKSHELF_REVIEW_DELAY_MS=1000 so that saving a
review takes about one second and the waits have something real to wait for. C#, Ruby and JavaScript snippets are shown where
the official documentation shows them; they were not run here because the examples were run in Java and Python only.
For the idea behind waiting (why a script races the page) read How Browser Automation Works; for how Playwright and Cypress avoid explicit waits, see Playwright Locators, Actions & Auto-Waiting and Cypress Core Concepts. Locators, element commands and frames are in Locating & Interacting; timeouts, page load strategies and driver options are in Drivers, Browsers & Options; the general strategy for reliable tests is in Flaky Tests & Reliability.
Why a script needs to wait
Every navigation command (get, back, refresh) waits for the document readyState that the page load strategy asks
for (the default is complete) before the driver returns. That only covers the assets named in the HTML. Scripts that run
afterwards add elements, change visibility or replace content, and a single-page application does most of its work after
load. Selenium can interact with an element only when it is both present in the DOM and displayed, so a command that runs
too early fails with NoSuchElementException or ElementNotInteractableException.
Bookshelf has a realistic case. After a signed-in reader submits a review, the new row appears at once with
data-state="pending", and about a second later, when the server answers, it becomes data-state="saved". A test that
looks for the saved row straight after the click races the server. The first example shows the failure, and every later
example fixes it with a different kind of wait.
The sign-in and review steps are shared by all examples, so they live in a small helper. Imports are omitted.
final class Bookshelf {
static final String BASE_URL = System.getenv().getOrDefault("BASE_URL", "http://localhost:3914");
static final String READER_PASSWORD =
System.getenv().getOrDefault("BOOKSHELF_READER_PASSWORD", "reader-local-only");
static final By SAVED_REVIEW = By.cssSelector("[data-testid=review][data-state=saved]");
static final By SUBMIT = By.cssSelector("[data-testid=review-submit]");
static ChromeOptions headless() {
ChromeOptions options = new ChromeOptions();
options.addArguments("--headless=new");
return options;
}
/** Signs in as the reader, opens a book and submits a review; the app saves it after a delay. */
static void submitReview(WebDriver driver, String text) {
driver.get(BASE_URL + "/signin");
driver.findElement(By.id("username")).sendKeys("reader");
driver.findElement(By.id("password")).sendKeys(READER_PASSWORD);
driver.findElement(By.cssSelector("[data-testid=signin-submit]")).click();
new WebDriverWait(driver, Duration.ofSeconds(5)).until(ExpectedConditions.urlContains("/account"));
driver.get(BASE_URL + "/books/3");
new Select(driver.findElement(By.id("rating"))).selectByValue("4");
driver.findElement(By.id("text")).sendKeys(text);
driver.findElement(SUBMIT).click();
}
}
Source: Selenium: Waiting Strategies
The Python equivalent is a pytest fixture in conftest.py that does the same steps and returns a submit function. The
reader password comes from BOOKSHELF_READER_PASSWORD, never from the test file.
@pytest.fixture
def submit_review(driver):
"""Signs in, opens a book and submits a review; the app saves it after a delay."""
def submit(text="Saved a moment later, after the delay."):
driver.get(f"{BASE_URL}/signin")
driver.find_element(By.ID, "username").send_keys("reader")
driver.find_element(By.ID, "password").send_keys(READER_PASSWORD)
driver.find_element(By.CSS_SELECTOR, "[data-testid=signin-submit]").click()
WebDriverWait(driver, 5).until(EC.url_contains("/account"))
driver.get(f"{BASE_URL}/books/3")
Select(driver.find_element(By.ID, "rating")).select_by_value("4")
driver.find_element(By.ID, "text").send_keys(text)
driver.find_element(By.CSS_SELECTOR, "[data-testid=review-submit]").click()
return submit
Source: Selenium: Waiting Strategies
The failure, and why sleeping is not the answer
@Test
void noWaitFails() {
Bookshelf.submitReview(driver, "Saved a moment later, after the delay.");
// The review is only "saved" after the server answers (about one second), so this fails.
assertThrows(org.openqa.selenium.NoSuchElementException.class,
() -> driver.findElement(Bookshelf.SAVED_REVIEW));
}
def test_no_wait_fails(driver, submit_review):
submit_review()
# The review is only "saved" after the server answers (about one second).
with pytest.raises(NoSuchElementException):
driver.find_element(*SAVED_REVIEW)
Source: Selenium: Waiting Strategies
Both tests pass because they assert the failure. The usual first fix is a fixed sleep. It is the wrong one: too short and the test is still flaky, too long and it is added in many places and the suite becomes slow. A sleep also never reports what it was waiting for. Selenium provides two mechanisms that are better, and one of them, the explicit wait, is the one to use.
Implicit waits
An implicit wait is a session-wide setting: while it is non-zero, every element lookup that finds nothing keeps retrying
until the element appears or the time runs out, and only then raises NoSuchElementException. The default is 0 seconds, so
by default a missing element fails immediately. The wait ends as soon as the element is found, so a large value does not
slow down the tests that do not need it. It applies to the whole session, to findElement and findElements
alike, and it is part of the W3C timeouts object next to the page load timeout (300 seconds by default) and the script
timeout (30 seconds by default).
@Test
void implicitWait() {
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(5));
Bookshelf.submitReview(driver, "Saved a moment later, after the delay.");
WebElement saved = driver.findElement(Bookshelf.SAVED_REVIEW); // blocks until the element exists
assertTrue(saved.getText().contains("Saved a moment later"));
}
def test_implicit_wait(driver, submit_review):
driver.implicitly_wait(5)
submit_review()
saved = driver.find_element(*SAVED_REVIEW) # blocks until the element exists
assert "Saved a moment later" in saved.text
Source: Selenium: Waiting Strategies
The same setting in the other bindings, as shown by the official documentation (these snippets were not run here):
| Binding | Implicit wait of two seconds |
|---|---|
C# |
|
Ruby |
|
JavaScript |
|
Source: Selenium: Waiting Strategies
The value can also be requested when the session is created, with the timeouts capability of the browser options. Both
checked here:
ChromeOptions options = new ChromeOptions();
options.setImplicitWaitTimeout(Duration.ofSeconds(5)); // also setPageLoadTimeout, setScriptTimeout
options = webdriver.ChromeOptions()
options.timeouts = {"implicit": 5000} # milliseconds; also "pageLoad" and "script"
driver = webdriver.Chrome(options=options)
print(driver.timeouts.implicit_wait, driver.timeouts.page_load, driver.timeouts.script) # 5.0 300.0 30.0
Source: Selenium: Waiting Strategies
Implicit waits are convenient, but they are blunt. They only wait for presence, never for visibility, enabled state, text or a count, they cannot be tuned per call, and they interact badly with explicit waits (next section).
Do not mix implicit and explicit waits
The official documentation is blunt about it: mixing the two can cause unpredictable wait times, for example an implicit
wait of ten seconds and an explicit wait of fifteen could time out after twenty. The reason is that an explicit wait polls
by calling findElement, and with an implicit wait set, every one of those calls can itself block for the implicit
timeout. The explicit wait only checks its own deadline between polls, so a poll that started before the deadline runs to
the end of the implicit wait.
This was measured against Bookshelf: an implicit wait of 6 seconds and an explicit wait of 2 seconds waiting for an element that never appears.
@Test
void mixingImplicitAndExplicitWaits() {
driver.get(Bookshelf.BASE_URL + "/books/3");
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(6));
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(2));
long start = System.nanoTime();
assertThrows(TimeoutException.class,
() -> wait.until(ExpectedConditions.presenceOfElementLocated(By.id("does-not-exist"))));
long seconds = Duration.ofNanos(System.nanoTime() - start).toSeconds();
System.out.println("MIXED WAIT: explicit 2 s + implicit 6 s gave up after about " + seconds + " s");
assertTrue(seconds >= 6, "the implicit wait stretched the explicit one");
}
def test_mixing_implicit_and_explicit_waits(driver, base_url):
driver.get(f"{base_url}/books/3")
driver.implicitly_wait(6)
start = time.monotonic()
with pytest.raises(TimeoutException):
WebDriverWait(driver, 2).until(EC.presence_of_element_located((By.ID, "does-not-exist")))
elapsed = time.monotonic() - start
print(f"MIXED WAIT: explicit 2 s + implicit 6 s gave up after {elapsed:.1f} s")
assert elapsed >= 6
MIXED WAIT: explicit 2 s + implicit 6 s gave up after about 6 s (Java)
MIXED WAIT: explicit 2 s + implicit 6 s gave up after 6.0 s (Python)
Source: Selenium: Waiting Strategies
The explicit wait asked for two seconds and took six. With several polls or nested conditions the overshoot multiplies.
The rule that follows: pick explicit waits and leave the implicit wait at 0. If a codebase already sets an implicit wait
in a base class, remove it, or set it back to zero around the explicit wait with
driver.manage().timeouts().implicitlyWait(Duration.ZERO).
Explicit waits
An explicit wait is a loop in your code that polls a condition until it returns something other than false or null,
or until a timeout, when it raises TimeoutException. It is the right tool because it names the condition for each place
that needs one. By default the wait also tolerates a missing element: Java’s WebDriverWait ignores
NotFoundException and Python’s WebDriverWait ignores NoSuchElementException, so a condition that looks the element up
keeps polling until it exists. The default polling interval is 500 milliseconds in both.
The condition can be an Expected Condition (next sections) or a function of the driver. Java also accepts any
java.util.function.Function, so a lambda works; Python accepts any callable.
@Test
void explicitWaitWithExpectedConditions() {
Bookshelf.submitReview(driver, "Saved a moment later, after the delay.");
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(5));
WebElement saved = wait.until(ExpectedConditions.visibilityOfElementLocated(Bookshelf.SAVED_REVIEW));
assertEquals("saved", saved.getDomAttribute("data-state"));
}
@Test
void explicitWaitWithLambda() {
Bookshelf.submitReview(driver, "Saved a moment later, after the delay.");
Wait<WebDriver> wait = new WebDriverWait(driver, Duration.ofSeconds(5));
WebElement saved = wait.until(d -> d.findElement(Bookshelf.SAVED_REVIEW));
assertTrue(saved.isDisplayed());
}
def test_explicit_wait_with_expected_condition(driver, submit_review):
submit_review()
saved = WebDriverWait(driver, timeout=5).until(EC.visibility_of_element_located(SAVED_REVIEW))
assert saved.get_dom_attribute("data-state") == "saved"
def test_explicit_wait_with_lambda(driver, submit_review):
submit_review()
saved = WebDriverWait(driver, timeout=5).until(lambda d: d.find_element(*SAVED_REVIEW))
assert saved.is_displayed()
Source: Selenium: Waiting Strategies
until returns whatever the condition returned (the element, a list, a boolean), so the result of the wait is usually the
object the next line needs. Bindings differ in spelling only. The official documentation shows:
| Binding | Explicit wait for a revealed element (not run here) |
|---|---|
C# |
|
Ruby |
|
JavaScript |
|
Source: Selenium: Waiting Strategies
needs no condition classes. JavaScript has its own until module, whose conditions return promises.
Timeouts and failure messages
When the timeout expires the wait raises TimeoutException. Its message starts with the condition that failed and, for
Expected Conditions, the underlying error, which is usually enough to see what went wrong:
@Test
void timeoutMessage() {
driver.get(Bookshelf.BASE_URL + "/books/3");
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(1));
TimeoutException e = assertThrows(TimeoutException.class,
() -> wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("does-not-exist"))));
System.out.println("TIMEOUT MESSAGE: " + e.getMessage().lines().findFirst().orElse(""));
}
def test_timeout_message(driver, base_url):
driver.get(f"{base_url}/books/3")
with pytest.raises(TimeoutException) as info:
WebDriverWait(driver, 1).until(EC.visibility_of_element_located((By.ID, "does-not-exist")), message="no such box")
print("TIMEOUT MESSAGE:", str(info.value).splitlines()[0])
TIMEOUT MESSAGE: Expected condition failed: waiting for visibility of element found by By.id: does-not-exist, but... org.openqa.selenium.NoSuchElementException: no such element: Unable to locate element: {"method":"css selector","selector":"#does\-not\-exist"}.
TIMEOUT MESSAGE: Message: no such box
Pass a message (message= in Python, withMessage in Java) to say in domain terms what was awaited; the Python example
shows that a custom message replaces the default one.
Fluent waits and customisation
The wait class can be customised in four ways: how often the condition is evaluated (the polling interval), which
exceptions are swallowed while polling, the total timeout, and the timeout message. Java exposes them on FluentWait
(WebDriverWait is a FluentWait<WebDriver> with sensible defaults), Python as keyword arguments of WebDriverWait, C#
as properties of WebDriverWait, and Ruby as keyword arguments of Wait.new.
@Test
void fluentWaitWithPollingAndIgnoredExceptions() {
Bookshelf.submitReview(driver, "Saved a moment later, after the delay.");
Wait<WebDriver> wait = new FluentWait<>(driver)
.withTimeout(Duration.ofSeconds(5))
.pollingEvery(Duration.ofMillis(200))
.ignoring(org.openqa.selenium.NoSuchElementException.class)
.withMessage("the review was not saved");
String author = wait.until(d -> d.findElement(Bookshelf.SAVED_REVIEW)
.findElement(By.cssSelector("[data-testid=review-author]")).getText());
assertEquals("reader", author);
}
def test_wait_with_polling_and_ignored_exceptions(driver, submit_review):
submit_review()
wait = WebDriverWait(
driver,
timeout=5,
poll_frequency=0.2,
ignored_exceptions=[NoSuchElementException, StaleElementReferenceException],
)
author = wait.until(
lambda d: d.find_element(*SAVED_REVIEW).find_element(By.CSS_SELECTOR, "[data-testid=review-author]").text
)
assert author == "reader"
Source: Selenium: Waiting Strategies
Both ran against Bookshelf and passed. The options in the official documentation, with the other bindings, are:
| Binding | Timeout | Polling interval | Ignored exceptions |
|---|---|---|---|
Java ( |
|
|
|
Python ( |
|
|
|
C# ( |
constructor argument or |
|
|
Ruby ( |
|
|
|
Source: Selenium: Waiting Strategies
The C# and Ruby forms in the table come from the official documentation and were not run here. The official page also
uses the idea of retrying an action: if "element not interactable" is retried by default, ignore
ElementNotInteractableException and put the action inside the condition, making sure the function returns true when
it succeeded. That turns a wait into a retry loop, which is useful for an element that exists but is still being
animated into place; a wait on elementToBeClickable is usually clearer.
Expected Conditions
Expected Conditions are ready-made conditions for explicit waits. Some take a locator, so the lookup is repeated on every poll and a stale element is not a problem; others take an element you already hold, which saves a lookup but fails with a stale element if the DOM is rebuilt. Prefer the locator forms for anything dynamic.
Java has them in ExpectedConditions (class org.openqa.selenium.support.ui), Python in the expected_conditions module
(imported by convention as EC), JavaScript in the until module of selenium-webdriver. .NET dropped them in Selenium 4
to minimise maintenance, and Ruby does not need them. The Java and Python catalogues of 4.51.0, as read from the released
packages, are:
| What is awaited | Java ExpectedConditions |
Python EC |
|---|---|---|
Page title and URL |
|
|
Element present |
|
|
Element visible |
|
|
Element hidden or gone |
|
|
Element clickable |
|
|
Text |
|
|
Attributes and properties |
|
|
Selection state |
|
|
Number of elements |
|
(use a lambda that counts |
Frames, alerts and windows |
|
|
JavaScript |
|
(use |
Combinators and retry |
|
|
Notes on the ones that cause most trouble:
-
visibilityOf...waits for the element to be displayed;presenceOf...only that it exists in the DOM. After an action that reveals something, wait for visibility. Presence is enough when you only read an attribute. -
elementToBeClickableis visible and enabled. It does not prove that nothing covers the element, so an overlay or a moving element can still causeElementClickInterceptedException. -
stalenessOfis true once an old element reference is detached, which is the reliable signal that a page reloaded or that a list was rebuilt.refreshedwraps a condition and retries it when it throwsStaleElementReferenceException. -
alertIsPresentreturns the alert, sowait.until(alertIsPresent()).accept()works. Alerts are covered in Locating & Interacting. -
Python’s
all_of,any_ofandnone_ofcombine conditions; Java’sand,orandnotreturn aBoolean.
The Python example below combines several of them against Bookshelf; it ran and passed.
def test_any_of_all_of_and_staleness(driver, base_url):
driver.get(f"{base_url}/books?q=quiet")
first = driver.find_element(By.CSS_SELECTOR, "[data-testid=book-card]")
wait = WebDriverWait(driver, 5)
wait.until(EC.all_of(EC.title_contains("Bookshelf"), EC.visibility_of(first)))
driver.refresh()
assert wait.until(EC.staleness_of(first))
with pytest.raises(StaleElementReferenceException):
first.text
wait.until(EC.any_of(EC.url_contains("quiet"), EC.title_is("never")))
The Java counterpart checks staleness and refreshed:
@Test
void stalenessAndRefreshed() {
driver.get(Bookshelf.BASE_URL + "/books?q=quiet");
WebElement firstCard = driver.findElement(By.cssSelector("[data-testid=book-card]"));
driver.navigate().refresh();
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(5));
assertTrue(wait.until(ExpectedConditions.stalenessOf(firstCard)));
assertThrows(StaleElementReferenceException.class, firstCard::getText);
WebElement again = wait.until(ExpectedConditions.refreshed(
ExpectedConditions.visibilityOfElementLocated(By.cssSelector("[data-testid=book-card]"))));
assertTrue(again.isDisplayed());
}
Writing your own condition
A condition is just a function of the driver that returns a non-null, non-false value when satisfied. Write one when the
page needs a check that no built-in condition covers, such as "at least one review is saved" or "the count of cards equals
what the API says". In Java an ExpectedCondition<T> is a functional interface, so a lambda is enough:
@Test
void customExpectedCondition() {
Bookshelf.submitReview(driver, "Saved a moment later, after the delay.");
// An ExpectedCondition is a java.util.function.Function<WebDriver, T>: a lambda is enough.
ExpectedCondition<Integer> savedReviewCount = d -> {
var saved = d.findElements(Bookshelf.SAVED_REVIEW);
return saved.isEmpty() ? null : saved.size();
};
Integer count = new WebDriverWait(driver, Duration.ofSeconds(5)).until(savedReviewCount);
assertEquals(1, count);
// Any JDK Function works too, without Guava on the type.
Function<WebDriver, WebElement> asFunction = ExpectedConditions.presenceOfElementLocated(Bookshelf.SAVED_REVIEW);
assertNotNull(asFunction.apply(driver));
}
In Python the same is a lambda or a small class with a __call__ method that takes the driver. Return False or None
to keep waiting, and something truthy when done.
Java 4.51: ExpectedCondition no longer implements Guava’s Function
Before Selenium 4.51, ExpectedCondition<T> extended two interfaces: com.google.common.base.Function and
java.util.function.Function. From 4.51 it extends only the JDK one:
public interface ExpectedCondition<T extends @Nullable Object>
extends java.util.function.Function<WebDriver, T> {}
The change is part of an effort, announced on 21 September 2026, to remove Guava from the Java client bindings. It is a
breaking change only for code that types a variable, field or parameter as Guava’s Function and assigns an
ExpectedCondition to it. Passing the result of an ExpectedConditions factory straight to until, or implementing the
interface with a lambda, is unaffected. Compiling the following against 4.51.0 confirms it (Guava is on the classpath,
so the Guava type resolves; the assignment is what fails):
// Before: compiled with 4.50 and earlier, fails with 4.51
com.google.common.base.Function<WebDriver, WebElement> condition =
ExpectedConditions.elementToBeClickable(By.id("submit"));
// After: a drop-in replacement, the apply method is identical
java.util.function.Function<WebDriver, WebElement> condition =
ExpectedConditions.elementToBeClickable(By.id("submit"));
Check.java:7: error: incompatible types: ExpectedCondition<WebElement> cannot be converted to Function<WebDriver,WebElement>
ExpectedConditions.elementToBeClickable(By.id("submit"));
To find affected code run grep -rn "com.google.common.base.Function" --include="*.java" . and replace the type with
java.util.function.Function, with ExpectedCondition<WebElement>, or let the compiler infer it with var. The
ExpectedCondition interface in the 4.51.0 jar was inspected with javap and extends only
java.util.function.Function<WebDriver, T>. One more fact from the same check: running mvn dependency:tree on a project
that depends on selenium-java 4.51.0 still lists Guava 33.7.2 as a transitive dependency of selenium-api, so the
library has not yet vanished from the classpath. Only this public type changed; do not rely on Guava being present or
absent through Selenium, and declare your own dependency if your own code uses Guava.
Listeners and event firing
Listeners run custom code whenever specific Selenium commands are sent, which is handy for logging, screenshots on failure or tracing a flaky test. The official page for command listeners has no examples at the time of writing, so this section is based on the API of the released packages, which was run in Java and Python.
Java: WebDriverListener and EventFiringDecorator
Implement WebDriverListener, an interface made of default methods named beforeX and afterX for every driver, element,
navigation, alert, window and options command, plus beforeAnyCall, afterAnyCall and onError. Override only what you
need, then wrap the driver with EventFiringDecorator:
public static class RecordingListener implements WebDriverListener {
final List<String> events = new ArrayList<>();
@Override public void beforeGet(WebDriver driver, String url) { events.add("before get " + url); }
@Override public void afterGet(WebDriver driver, String url) { events.add("after get"); }
@Override public void beforeClick(WebElement element) { events.add("before click " + element.getDomAttribute("data-testid")); }
@Override public void afterSendKeys(WebElement element, CharSequence... keys) { events.add("sent keys to " + element.getDomAttribute("id")); }
@Override public void onError(Object target, Method method, Object[] args, InvocationTargetException e) {
events.add("error in " + method.getName() + ": " + e.getTargetException().getClass().getSimpleName());
}
}
@Test
void eventFiringDecorator() {
RecordingListener listener = new RecordingListener();
WebDriver driver = new EventFiringDecorator<WebDriver>(listener).decorate(Bookshelf.newDriver());
try {
driver.get(Bookshelf.BASE_URL + "/books");
driver.findElement(By.cssSelector("[data-testid=search-input]")).sendKeys("quiet");
driver.findElement(By.cssSelector("[data-testid=search-submit]")).click();
assertThrows(NoSuchElementException.class, () -> driver.findElement(By.id("missing")));
listener.events.forEach(e -> System.out.println("EVENT: " + e));
assertTrue(listener.events.contains("before click search-submit"));
} finally {
driver.quit();
}
}
EVENT: before get http://localhost:3914/books
EVENT: after get
EVENT: sent keys to q
EVENT: before click search-submit
EVENT: error in findElement: NoSuchElementException
Two details from running it. First, the listener class has to be public: with a package-private nested class the
decorator silently did not call the before and after methods, and only onError fired. Second, findElement returns
decorated elements, so listeners on click and sendKeys fire for elements found through the decorated driver, but not for
elements you obtained from the undecorated one. The decorator replaces the older EventFiringWebDriver and
WebDriverEventListener, which are not in the 4.51.0 selenium-support jar. Several listeners can be passed to the
constructor, and EventFiringDecorator can be extended to decorate other driver types.
Python: EventFiringWebDriver and AbstractEventListener
Python keeps EventFiringWebDriver and AbstractEventListener (module selenium.webdriver.support). Subclass the
listener and override the hooks you need: before_navigate_to, after_navigate_to, before_navigate_back,
after_navigate_back, before_navigate_forward, after_navigate_forward, before_find, after_find, before_click,
after_click, before_change_value_of, after_change_value_of, before_execute_script, after_execute_script,
before_close, after_close, before_quit, after_quit and on_exception.
class RecordingListener(AbstractEventListener):
def __init__(self):
self.events = []
def before_navigate_to(self, url, driver):
self.events.append(f"before navigate {url}")
def after_navigate_to(self, url, driver):
self.events.append("after navigate")
def before_click(self, element, driver):
self.events.append(f"before click {element.get_dom_attribute('data-testid')}")
def after_change_value_of(self, element, driver):
self.events.append(f"changed value of {element.get_dom_attribute('id')}")
def on_exception(self, exception, driver):
self.events.append(f"error: {type(exception).__name__}")
def test_event_firing_webdriver(driver, base_url):
listener = RecordingListener()
wrapped = EventFiringWebDriver(driver, listener)
wrapped.get(f"{base_url}/books")
wrapped.find_element(By.CSS_SELECTOR, "[data-testid=search-input]").send_keys("quiet")
wrapped.find_element(By.CSS_SELECTOR, "[data-testid=search-submit]").click()
with pytest.raises(NoSuchElementException):
wrapped.find_element(By.ID, "missing")
assert "before click search-submit" in listener.events
EVENT: before navigate http://localhost:3914/books
EVENT: after navigate
EVENT: changed value of q
EVENT: before click search-submit
EVENT: error: NoSuchElementException
The event-firing and listener wrappers for C#, Ruby and JavaScript were not run; consult the API documentation of each binding for its listener types, since the official page for command listeners shows no code for them.
ThreadGuard
ThreadGuard exists only in the Java binding. It wraps a driver and checks that every call comes from the thread that
created it, raising a WebDriverException otherwise. WebDriver instances are not thread-safe, and misuse in parallel runs
produces mysterious failures that are hard to diagnose; the guard turns them into a clear error at the first bad call.
@Test
void threadGuardRejectsAnotherThread() throws Exception {
WebDriver driver = ThreadGuard.protect(Bookshelf.newDriver());
try {
AtomicReference<Throwable> thrown = new AtomicReference<>();
Thread other = new Thread(() -> {
try {
driver.get(Bookshelf.BASE_URL);
} catch (Throwable t) {
thrown.set(t);
}
});
other.start();
other.join();
assertInstanceOf(WebDriverException.class, thrown.get());
driver.get(Bookshelf.BASE_URL); // the creating thread still works
} finally {
driver.quit();
}
}
Thread safety error; this instance of WebDriver was constructed on thread main (id 1) and is being accessed by thread Thread-46 (id 228)
This is not permitted and *will* cause undefined behaviour
Source: Selenium: ThreadGuard
ThreadGuard does not replace the need to give each parallel test its own driver, normally with a ThreadLocal or a
per-test fixture. With JUnit 5 parallel execution, create the driver in @BeforeEach and quit it in @AfterEach, as the
examples on this page do, and wrap it with ThreadGuard.protect if you want the safety net.
Troubleshooting
The official troubleshooting section starts with the observation that the most common Selenium error is poor synchronisation. If you are not sure that a failure is a timing problem, hard-code a long sleep where it fails: if the failure disappears, an explicit wait will fix it (then remove the sleep). Many reports are caused by the browser drivers rather than Selenium itself, which you can rule out by running the command in more than one browser. For questions, see the support options of the project; for a bug in Selenium, file a bug report on GitHub. The "Upgrade to Selenium 4" page of this section is covered in Selenium IDE & Legacy.
Understanding common errors
The W3C specification defines the errors a driver can return, and each binding maps them to its own class. Kotlin uses the Java classes. The mapping in the official documentation is:
| W3C error | Java | Python | C# | Ruby | JavaScript |
|---|---|---|---|---|---|
detached shadow root |
|
|
|
|
|
element click intercepted |
|
|
|
|
|
element not interactable |
|
|
|
|
|
insecure certificate |
|
|
|
|
|
invalid argument |
|
|
|
|
|
invalid cookie domain |
|
|
|
|
|
invalid element state |
|
|
|
|
|
invalid selector |
|
|
|
|
|
invalid session id |
|
|
|
|
|
javascript error |
|
|
|
|
|
move target out of bounds |
|
|
|
|
|
no such alert |
|
|
|
|
|
no such cookie |
|
|
|
|
|
no such element |
|
|
|
|
|
no such frame |
|
|
|
|
|
no such shadow root |
|
|
|
|
|
no such window |
|
|
|
|
|
script timeout |
|
|
|
|
|
session not created |
|
|
|
|
|
stale element reference |
|
|
|
|
|
timeout |
|
|
|
|
|
unable to capture screen |
|
|
|
|
|
unable to set cookie |
|
|
|
|
|
unexpected alert open |
|
|
|
|
|
unknown command |
|
|
|
|
|
unknown error |
|
|
|
|
|
unknown method |
|
|
|
|
|
unsupported operation |
|
|
|
|
|
All of these inherit from the base exception of their binding (WebDriverException in Java, Python and C#,
WebDriverError in Ruby and JavaScript), except the C# InvalidOperationException and NotImplementedException, which
are .NET system exceptions. Java’s NoSuchElementException, StaleElementReferenceException and other
element errors live in org.openqa.selenium; note that java.util.NoSuchElementException is a different class, so
import the right one (the examples above spell out org.openqa.selenium.NoSuchElementException where both could be
imported). The causes and fixes, grouped by the likelihood of meeting them:
| Error | Likely cause | What to do |
|---|---|---|
|
Wrong place (an earlier step failed), wrong time (the element is not in the DOM yet), or the locator changed. |
Check that the page is the expected one, use a proper explicit wait, and re-check the locator in the browser’s developer tools. |
|
The element was found earlier but is no longer in the current DOM: the page refreshed or re-rendered, you navigated, or you switched window or frame. |
Locate the element again each time it is used, or wrap it in an object that stores the locator and relocates on a stale
error. If the context changed, switch back to the right window or frame; if the page changed, navigate back and locate
again. Use |
|
Another element would receive the click: a sticky header, a modal or an animation. |
Wait with |
|
The action is not supported by the element ( |
Use the right action, make the locator unique, scroll into view and wait until the element is interactable. |
|
A malformed CSS or XPath expression, or an XPath given to a CSS strategy. |
Validate the selector in the browser’s developer tools. |
|
An explicit wait expired (the usual cause, raised by the binding rather than the driver), or the page took longer than the page load timeout (300 seconds by default). |
Check that the awaited condition can happen, raise the timeout only if the application is legitimately slow, or use a different page load strategy. |
|
Browser and driver versions do not match, macOS privacy settings block the driver, or the driver binary is missing or not executable. |
Match the driver to the browser (Selenium Manager does this for you), allow the driver in System Settings, and make the
binary executable with |
|
The session was deleted by |
Look for |
|
The window or tab was closed or the handle is wrong. |
After closing a window, switch to another one; list the window handles to check. |
|
The frame has not loaded yet, the locator or index is wrong, or nested frames were not entered one at a time. |
Wait with |
|
The dialog has not opened, was already closed, or is an HTML modal; or an unexpected dialog blocks the command. |
Wait with |
|
A self-signed, expired or mismatched TLS certificate in a test environment. |
Set |
|
A wrong type or malformed value: a relative URL passed to |
Read the message; it normally names the argument. |
|
A cookie added before navigating to its domain, with invalid values, or secure on a non-HTTPS page; or a cookie that does not exist for the page. |
Navigate to the domain first, check the values and the scheme. |
|
The element is disabled or read-only, for example when clearing it. |
Wait for the application to enable it. |
|
A script failed in the browser, or an asynchronous script never called its callback within the script timeout (30 seconds by default). |
Debug the script in the developer tools, call the callback on every path, and prefer WebDriver commands to scripts. |
|
A pointer action’s offset puts it outside the viewport. |
Check the offsets, scroll first and use a bigger window. |
|
The shadow root’s host was removed or replaced; or the element is not a shadow host or the root is not attached yet. |
Find the host again and take its shadow root again; wait until it is attached. |
|
The page or element is too large for the browser to capture. |
Capture the viewport or a smaller element. |
|
The command is not in the W3C specification, or not implemented by that driver or its version; or an unspecific driver error such as a crashed browser. |
Update the browser and driver, read the full message and turn on logging. |
ElementNotVisibleException is a legacy name that is not part of the W3C specification; current drivers return
ElementNotInteractableException instead, and only the Python binding still defines the old class for backward
compatibility. The page also links an interactive Selenium Wait Code Generator for building explicit wait snippets.
Five of these errors were reproduced against Bookshelf in Java and Python (both ran and passed):
@Test
void commonErrors() {
driver.get(Bookshelf.BASE_URL + "/books/3");
// NoSuchElementException: wrong place or wrong time.
assertThrows(NoSuchElementException.class, () -> driver.findElement(By.id("nope")));
// InvalidSelectorException: an XPath expression given to a CSS strategy.
assertThrows(InvalidSelectorException.class, () -> driver.findElement(By.cssSelector("//h1")));
// ElementNotInteractableException: the sign-in error message is in the DOM but
// hidden until a sign-in fails.
driver.get(Bookshelf.BASE_URL + "/signin");
WebElement hidden = driver.findElement(By.cssSelector("[data-testid=signin-error]"));
assertThrows(ElementNotInteractableException.class, () -> hidden.sendKeys("text"));
// StaleElementReferenceException: the page changed after the element was found.
WebElement username = driver.findElement(By.id("username"));
driver.navigate().refresh();
assertThrows(StaleElementReferenceException.class, username::getText);
// TimeoutException is raised by the wait, not by the driver.
assertThrows(TimeoutException.class, () -> new WebDriverWait(driver, Duration.ofMillis(500))
.until(ExpectedConditions.visibilityOfElementLocated(By.id("nope"))));
}
def test_common_errors(driver, base_url):
driver.get(f"{base_url}/books/3")
with pytest.raises(NoSuchElementException):
driver.find_element(By.ID, "nope")
with pytest.raises(InvalidSelectorException):
driver.find_element(By.CSS_SELECTOR, "//h1")
driver.get(f"{base_url}/signin")
hidden = driver.find_element(By.CSS_SELECTOR, "[data-testid=signin-error]")
with pytest.raises(ElementNotInteractableException):
hidden.send_keys("text")
The remaining errors in the tables (click intercepted, session not created, shadow root, certificate and so on) are described from the official documentation and were not reproduced here.
Unable to locate driver
Historically the most common first-run error. It shows as one of these messages, depending on the binding and browser: the
path to the driver executable must be set by the webdriver.chrome.driver system property; the executable chromedriver
needs to be available in the path; the file geckodriver does not exist; or "Unable to locate the chromedriver
executable". It means Selenium needed a driver to talk to the browser and none of the ways it looks for one succeeded.
The solutions, in order of preference:
-
Use a current Selenium. Since Selenium 4.6, Selenium Manager downloads the right driver (and, since 4.11, the browser) automatically, so no setup is needed. If you get the error with a recent version, turn on logging (below) and file a bug report with the log. Selenium Manager and its configuration are described in Getting Started.
-
Put the driver on the
PATH. Download the driver manually, place it in a directory already onPATHor add its directory (export PATH=$PATH:/path/to/driverin~/.bash_profileor~/.zshenv, orsetx PATHon Windows), and check withchromedriver --version. This changes the location without changing code and works across machines. -
Give the path in a
Serviceobject. Create the driver’s service class and set the executable path. It needs no environment variables but makes the code less portable. -
Use a third-party manager, only if you cannot use Selenium Manager (an old Selenium, or a feature it lacks): WebDriverManager for Java, WebDriver Manager for Python, a WebDriver Manager package for .NET, and the webdrivers gem for Ruby.
ChromeDriverService service = new ChromeDriverService.Builder()
.usingDriverExecutable(new File("/path/to/chromedriver"))
.build();
WebDriver driver = new ChromeDriver(service, new ChromeOptions());
service = webdriver.ChromeService(executable_path="/path/to/chromedriver")
driver = webdriver.Chrome(service=service)
These two snippets are the documented way to set the driver path and were not run here, because the Selenium Manager download is the setup used by every run on this page. The driver for each browser is maintained by the browser vendor: Chromium and Chrome by Google, Firefox (geckodriver) by Mozilla, Edge by Microsoft, Internet Explorer by the Selenium project and Safari by Apple (built into macOS High Sierra and newer). The Opera driver is no longer supported.
Logging Selenium commands
Turning logging on is often the quickest way to see what Selenium sent and received. Each binding uses its platform’s standard logging, so the names and levels differ:
| Binding | Logger and levels (default) | Default output | Mapping of Selenium messages |
|---|---|---|---|
Java |
|
|
Actionable items and deprecations: WARNING. Useful information: INFO. Debugging: FINE, FINER and FINEST. |
Python |
|
|
WARNING for deprecations, INFO, DEBUG. |
C# |
static |
|
Warn, Info, Debug. |
Ruby |
|
standard output; set |
warn for deprecations, info, one level (debug) for details. |
JavaScript |
|
none until |
INFO and below. |
Selenium’s policy is to mark an API deprecated for three releases and then remove it, and deprecations are logged as
warnings, so a clean log at the warning level is a cheap way to see what will break on upgrade. Java logging has two
traps. Setting the level on the root logger is not enough; you must also set the level of the root handlers (default
INFO), and you usually want to raise detail only for the classes you care about, such as RemoteWebDriver (every command
and every HTTP request) and SeleniumManager. The official documentation also points to the separate Selenium Logger
project for an easier setup in Java.
@Test
void loggingToAFile() throws Exception {
Path output = Files.createTempFile("selenium", ".log");
Logger root = Logger.getLogger("");
Level previous = root.getLevel();
root.setLevel(Level.FINE);
for (Handler h : root.getHandlers()) h.setLevel(Level.FINE);
Handler handler = new FileHandler(output.toString());
handler.setFormatter(new java.util.logging.SimpleFormatter());
root.addHandler(handler);
Logger.getLogger(RemoteWebDriver.class.getName()).setLevel(Level.FINEST);
try {
driver.get(Bookshelf.BASE_URL);
Logger.getLogger("demo").info("this is useful information");
Logger.getLogger("demo").fine("this is detailed debug information");
} finally {
root.removeHandler(handler);
handler.close();
root.setLevel(previous);
for (Handler h : root.getHandlers()) h.setLevel(Level.INFO);
}
assertTrue(Files.readString(output).contains("this is detailed debug information"));
}
Oct 25, 2026 9:44:00 PM org.openqa.selenium.remote.RemoteWebDriver log
FINE: Executing: get [d4977c173d8ebcc765e667bd34802b5c, get {url=http://localhost:3914}]
FINE: Executing request: (POST) /session/d4977c173d8ebcc765e667bd34802b5c/url {
Oct 25, 2026 9:44:01 PM org.openqa.selenium.remote.RemoteWebDriver log
INFO: this is useful information
FINE: this is detailed debug information
The log language follows the JVM locale: on the host used here it printed Spanish level names until the test was run with
-Duser.language=en. The file had 181 lines for one get. In Python the logger is the selenium package logger, and
sub-packages can have their own level:
def test_logging_to_a_file(driver, base_url, tmp_path):
log_path = tmp_path / "selenium.log"
logger = logging.getLogger("selenium")
logger.setLevel(logging.DEBUG)
handler = logging.FileHandler(log_path)
handler.setFormatter(logging.Formatter("%(levelname)s %(name)s: %(message)s"))
logger.addHandler(handler)
logging.getLogger("selenium.webdriver.remote").setLevel(logging.DEBUG)
try:
driver.get(base_url)
logger.info("this is useful information")
logger.debug("this is detailed debug information")
finally:
logger.removeHandler(handler)
handler.close()
lines = log_path.read_text().splitlines()
assert any("this is detailed debug information" in line for line in lines)
DEBUG selenium.webdriver.remote.remote_connection: POST http://localhost:64299/session/d15d1016eade3072f6f8fe3cc0e523c5/url {'url': 'http://localhost:...
DEBUG selenium.webdriver.remote.remote_connection: Remote response: status=200 | data={"value":null} | headers=HTTPHeaderDict({'Content-Length': '14', ...
DEBUG selenium.webdriver.remote.remote_connection: Finished Request
INFO selenium: this is useful information
DEBUG selenium: this is detailed debug information
With pytest there is one more trap: pytest captures logging and hides it unless the test fails. To see logs from passing
tests run pytest -s -p no:logging, which stops the console capture and pytest’s own logging plugin, and then call
logging.basicConfig(level=logging.WARN) yourself. The examples above log to a file instead, so they need none of that.
Filtering works at the same granularity as the logger: per class in Java and C# (Log.SetLevel(typeof(RemoteWebDriver), LogEventLevel.Debug)), per module in Python, and in Ruby by message id: every Selenium message has an id, and
logger.ignore(:jwp_caps, :logger_info) turns off deprecation notices while logger.allow(%i[selenium_manager example_id])
opts in. The C#, Ruby and JavaScript logging snippets (Log.Handlers.Add(new FileLogHandler(path)),
Selenium::WebDriver.logger.level = :debug, logging.getLogger('webdriver').setLevel(logging.Level.INFO)) come from the
official documentation and were not run here.
Choosing a wait strategy
-
Use explicit waits for everything that is not instantly present, name the condition, and keep the implicit wait at 0.
-
Wait for a user-visible state change, such as
data-state="saved", not for elapsed time. Where the application has no such signal, add one (adata-stateattribute or a live region) rather than adding sleeps. -
Prefer Expected Conditions with locators over conditions on stored elements; they survive re-rendering.
-
Give long waits a message that says what was awaited, and keep waits in page objects so tests read as intentions (see Test Design & Page Objects).
-
Keep one driver per thread, and use
ThreadGuardin Java if tests run in parallel. -
A wait cannot fix a test that depends on state left by another; see Flaky Tests & Reliability.
-
Asynchronous BiDi events (network, console, script) can replace polling for some conditions; see WebDriver BiDi.
What changed recently
| Version | Change |
|---|---|
Selenium 4.0 |
|
Selenium 4.6 |
Selenium Manager downloads the matching driver automatically, which makes the "unable to locate driver" error rare. |
Selenium 4.10 |
The Ruby logger gains the level, output, |
Selenium 4.11 |
Selenium Manager also downloads the browser when it is missing. |
Selenium 4.51 (9 October 2026) |
Java |
The common errors page was extended on 10 October 2026 to document all the W3C WebDriver errors; the table above reflects that revision.