ScreenshotNeo

BlogHow-to

How to Use JavaScript Waits in Selenium WebDriver

Use Selenium WebDriver’s JavaScript waits to synchronize with elements and application state. Learn when to use `driver.wait`, custom conditions, and `executeAsyncScript`.

By the ScreenshotNeo team4 October 20268 min read

In Selenium WebDriver’s JavaScript bindings, use driver.wait(condition, timeout) to wait until the page reaches the state required by your next command. Use Selenium’s until conditions for common checks such as locating or displaying an element, and a custom function for application-specific state. Use executeAsyncScript when asynchronous work must run inside the browser and explicitly call Selenium’s injected completion callback.

Navigation completing does not mean a JavaScript application has rendered the control you need. Choose a condition that matches the next action: an element can exist in the DOM but still be hidden or otherwise not ready to use.

Install and set up the JavaScript binding

Install Selenium WebDriver in a Node.js project:

npm install selenium-webdriver

The Selenium JavaScript overview retrieved for this guide documents Node.js 22 or later. Node and Selenium requirements can change, so check the official JavaScript overview for your installed release: Selenium WebDriver getting started.

The examples below use the JavaScript binding, async functions, and an already available browser driver. Here is a complete example using Chrome’s driver service:

const { Builder, By, until } = require('selenium-webdriver');
const chrome = require('selenium-webdriver/chrome');

async function main() {
  const options = new chrome.Options();
  // Uncomment for headless execution:
  // options.addArguments('--headless=new');

  const driver = await new Builder()
    .forBrowser('chrome')
    .setChromeOptions(options)
    .build();

  try {
    await driver.get('https://www.selenium.dev/');

    const link = await driver.wait(
      until.elementLocated(By.css('a[href*="documentation"]')),
      10_000,
      'Documentation link did not appear'
    );

    await driver.wait(
      until.elementIsVisible(link),
      5_000,
      'Documentation link was present but not visible'
    );

    console.log(await link.getText());
  } finally {
    await driver.quit();
  }
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

This assumes the browser and compatible driver are installed or available to Selenium’s driver management in your environment. If your setup provisions the driver separately, configure that according to your browser and deployment environment.

Wait for an element to be located

Use until.elementLocated(locator) when the element may not yet exist in the DOM. The resolved value is the located WebElement, so you can use it in the next step:

const { By, until } = require('selenium-webdriver');

const submit = await driver.wait(
  until.elementLocated(By.id('submit')),
  10_000,
  'Submit button was not added to the DOM'
);

await submit.click();

Location establishes presence, not visibility or interactability. If the application inserts the element before revealing it, follow the location wait with a visibility wait.

Wait for visibility before interacting

For a known WebElement, until.elementIsVisible(element) waits until Selenium considers it displayed:

const field = await driver.findElement(By.id('revealed'));

await driver.wait(
  until.elementIsVisible(field),
  2_000,
  'Field did not become visible'
);

await field.sendKeys('ready');

This example assumes the element can already be found. If it may be added later, first wait for location, then visibility:

const field = await driver.wait(
  until.elementLocated(By.id('revealed')),
  10_000,
  'Field was not added'
);

await driver.wait(
  until.elementIsVisible(field),
  5_000,
  'Field remained hidden'
);

await field.sendKeys('ready');

Visibility is still not a guarantee that every interaction will succeed. For example, an overlay or application state can prevent a click even when the target is displayed. Wait for the state relevant to the interaction and handle interaction failures with useful context.

Choose the wait that matches the next step

Need Use What it establishes
Element is in the DOM until.elementLocated(locator) The locator found a matching element.
Known element is displayed until.elementIsVisible(element) Selenium’s visibility condition is satisfied.
Application is ready according to its own state driver.wait(async () => ...) Your function returned a truthy result.
Browser-side asynchronous work completed executeAsyncScript The injected callback was invoked.
Pause for a fixed duration driver.sleep(ms) Only that amount of time elapsed.

Prefer a condition-based wait for readiness. A fixed sleep does not check whether the page is ready: it can waste time when the page is fast and still fail when the page is slower than the chosen delay.

Wait for application-specific state

When Selenium’s built-in conditions do not describe the state your app needs, pass a function to driver.wait. Return a truthy value only when the following operation can proceed. This example polls a data attribute set by the application:

await driver.wait(async () => {
  return await driver.executeScript(
    'return document.querySelector("#app")?.dataset.state === "ready"'
  );
}, 10_000, 'Application did not report ready state');

A condition can return a promise; Selenium waits for it to resolve, and the condition’s resolution time counts toward the timeout. Keep each check focused and avoid making it perform an action that should happen only once, since the condition may be evaluated repeatedly.

You can also return a useful value when ready, such as the matching element, rather than just true:

const readyButton = await driver.wait(async () => {
  const buttons = await driver.findElements(By.css('#app button.submit'));
  if (buttons.length === 0) return false;

  const button = buttons[0];
  return (await button.isDisplayed()) ? button : false;
}, 10_000, 'Ready submit button did not appear');

await readyButton.click();

Returning false keeps polling; returning a truthy element completes the wait with that element. If your custom condition uses element lookup, remember that any configured implicit wait also affects those lookups.

Use executeAsyncScript for page-context asynchronous work

executeAsyncScript runs in the selected browser frame and window. Selenium appends a callback argument to the script; invoke it when the page-side operation has completed. For example:

const result = await driver.executeAsyncScript((done) => {
  window.setTimeout(() => done('complete'), 500);
});

console.log(result);

The callback is the completion signal. If an asynchronous success or failure path never calls it, the script remains pending until Selenium’s script timeout interrupts it. This API is useful when the asynchronous operation itself needs to happen inside the page context. It is not the usual way to wait for an element; use driver.wait with an element or application-state condition for that.

Some examples pass a string and obtain the callback using arguments[arguments.length - 1]. Function serialization and argument handling depend on the binding behavior, so check the API for your installed version if using that style.

Configure timeouts and avoid wait conflicts

There are separate timeout concerns:

  • Explicit wait timeout: the duration passed to driver.wait(condition, timeout).
  • Script timeout: the maximum time Selenium allows an executing asynchronous browser script to run.
  • Implicit wait: a global delay applied to element-location calls.

Set the script timeout deliberately if your flow depends on executeAsyncScript:

await driver.manage().setTimeouts({ script: 15_000 });

const value = await driver.executeAsyncScript((done) => {
  window.setTimeout(() => done('finished'), 1_000);
});

The generated Selenium JavaScript API reference retrieved for this guide lists a 30,000 ms default script timeout. Defaults may vary by release, so set a value that fits your operation rather than relying on an assumed default. See the Selenium JavaScript API reference.

Avoid casually combining implicit and explicit waits. Selenium warns that their interaction can produce unpredictable elapsed times. For flows based on explicit conditions, keep implicit wait at zero and make each readiness wait explicit:

await driver.manage().setTimeouts({ implicit: 0 });

Choose a timeout long enough for expected variability in your environment, but short enough to fail usefully when the application is stuck. Include a message that identifies the missing state so a timeout points to a likely cause.

Common errors and fixes

Symptom Likely cause Fix
Element not found immediately after navigation Navigation completed at its configured ready state, but app JavaScript has not yet created the element. Wait for the specific locator or meaningful application state.
Element is found but click or typing fails Presence does not establish visibility or readiness for that interaction. Wait for visibility and, when needed, the application condition that enables the control.
Wait takes much longer than its timeout suggests An implicit wait inside element lookups is interacting with the explicit wait. Use a deliberate implicit-wait policy; for explicit waits, usually set implicit wait to zero.
Async script times out A completion path did not invoke Selenium’s callback, or the script timeout is too short. Call the callback on every completion path and set a suitable script timeout.
Fixed sleeps make tests slow or flaky The delay is disconnected from actual readiness. Replace the sleep with a condition that is polled until true or timed out.
Custom wait never resolves The function never returns a truthy result, checks the wrong state, or rejects/errors on each poll. Log or inspect the state being checked, make the condition return a useful truthy value, and provide a timeout message.

Reliability, performance, and cost

  • Reliability: synchronize on the state needed by the next command. A page-load readiness state only covers document loading; application code can continue changing the page afterward.
  • Performance: condition waits finish as soon as the condition succeeds, while fixed sleeps always consume their full duration. Keep custom polling checks lightweight and avoid actions with side effects inside the condition.
  • Timeouts: an explicit wait bounds how long a readiness check should take; script timeout bounds asynchronous script execution. They solve different problems.
  • Cost: Selenium’s wait choice has no per-wait service charge in the API. The practical cost is browser and test-run time, especially when fixed sleeps or unnecessarily long timeouts accumulate across a suite.

Or skip the browser setup

If your goal is to capture a page image or PDF rather than interact with it, ScreenshotNeo provides a website screenshot API and MCP server. Its API accepts a URL in one GET request and returns an image or PDF. See the ScreenshotNeo API documentation.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await require('node:fs/promises').writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
  • Cookie banners, popups, and chat widgets are removed before the shot; each cleanup step can be turned off.
  • Bot checks, blank pages, timeouts, failed loads, and cache hits are never billed. Responses identify the page verdict and billing status in headers.
  • An MCP server gives AI agents tools to take screenshots, get page information, and capture PDFs.
  • 1,000 screenshots a month are free with no card. Paid plans start at $5 for 3,000 shots; every feature is on every plan.

Sign up for 1,000 free screenshots a month, no card required.

FAQ

Does driver.get wait for JavaScript-rendered content?

It waits according to the configured page-load strategy and document readiness, which does not guarantee that application JavaScript has rendered the element your test needs. Add a wait for that element or app state.

Should I use executeAsyncScript to wait for an element?

Usually no. Use driver.wait with a locator, visibility condition, or custom state check. Use executeAsyncScript when asynchronous work runs in the page and signals completion through its callback.

Can a driver.wait condition be asynchronous?

Yes. Selenium’s JavaScript API accepts functions and thenables as conditions. A promise-like condition’s resolution time counts toward the wait timeout.

Why does an element-location wait not make a click safe?

Location checks DOM presence. It does not prove that the element is visible or that the application is ready for the click. Wait for the condition that matches the interaction.