How to Handle JavaScript Async and Await in Selenium
Use async/await for Selenium JavaScript commands, condition-based waits for UI readiness, and executeAsyncScript when page code must report an asynchronous result.
In Selenium’s JavaScript binding, mark your test or helper async and await WebDriver calls because they return promises. Use driver.wait() to wait for a meaningful page condition. Use executeAsyncScript() only when JavaScript running inside the page must do asynchronous work and return a result through Selenium’s callback.
These are two different kinds of waiting: await in Node.js waits for a WebDriver command promise; executeAsyncScript waits for page code to call a callback injected by Selenium. The examples below use the selenium-webdriver JavaScript package. Method names and timeout configuration differ in other language bindings.
1. Set up an async Selenium JavaScript test
Install Selenium’s JavaScript binding and the browser driver required by your environment. The exact browser and driver setup depends on your operating system and Selenium version; consult the Selenium JavaScript API documentation for the binding in use.
npm install selenium-webdriver
Here is a complete test-runner example. It opens a page, waits for a button to exist, clicks it, waits for the title to change, and always attempts to close the browser session.
const { Builder, By, until } = require('selenium-webdriver');
async function main() {
const driver = await new Builder().forBrowser('chrome').build();
try {
await driver.get('https://example.test');
const continueButton = await driver.wait(
until.elementLocated(By.id('continue')),
10_000,
'Continue button was not added to the page'
);
await continueButton.click();
await driver.wait(until.titleIs('Next step'), 10_000);
} finally {
await driver.quit();
}
}
main().catch(error => {
console.error(error);
process.exitCode = 1;
});
Each await makes the next step run after the preceding promise resolves. If a command rejects, the error is thrown at that await and can be handled with try/catch. Keeping browser shutdown in finally prevents an earlier failure from skipping cleanup.
What should be awaited?
In JavaScript WebDriver code, await the promise-returning operations whose results or completion your next step depends on. Common examples include navigation, element lookup, clicks, reads, waits, script execution, and session shutdown. If you omit await, later code may run before the command finishes, and a rejection may not be handled where you expect.
// Correct: wait for navigation before searching the new page.
await driver.get('https://example.test');
const heading = await driver.findElement(By.css('h1'));
const text = await heading.getText();
// Risky: the lookup may start before navigation has completed.
driver.get('https://example.test');
const headingTooSoon = await driver.findElement(By.css('h1'));
Use async on the function containing await. At the top level, whether you can write top-level await depends on your Node.js module format and runtime configuration; a named async function works in both CommonJS and ES module projects.
2. Wait for application state, not elapsed time
A command resolving does not necessarily mean a client-side application has finished rendering the state your test needs. Use a condition-based wait for the relevant state. Selenium’s JavaScript wait API checks conditions repeatedly and accepts promise-like conditions; it resolves when the condition becomes truthy or rejects on timeout. See the JavaScript API reference.
const { By, until } = require('selenium-webdriver');
await driver.get('https://example.test');
// Wait for presence in the DOM.
const result = await driver.wait(
until.elementLocated(By.css('[data-testid="results"]')),
10_000
);
// Wait for a visible element when visibility is the actual requirement.
await driver.wait(async () => {
const elements = await driver.findElements(By.css('[data-testid="ready"]'));
if (elements.length === 0) return false;
return elements[0].isDisplayed();
}, 10_000, 'Ready indicator did not become visible');
Choose the condition that matches what the next action requires: element located, visible, enabled, text updated, URL changed, or another observable state. Presence in the DOM alone does not prove that an element is visible or interactable.
When is a fixed delay appropriate?
A fixed delay waits for a duration; it does not verify that the page is ready. Avoid using it as a substitute for a readiness condition, since it can waste time on fast runs and still be too short on slow ones. A bounded delay can make sense when the behavior under test is specifically time-based, such as checking a transient message after a known interval. Prefer a condition wait for ordinary UI synchronization.
3. Run asynchronous JavaScript inside the page
driver.executeAsyncScript() is for a different execution context: Selenium serializes the supplied function and runs it in the selected page frame or window. Selenium supplies a completion callback as the function’s final argument. Page code must call that callback with the result; if it does not, the script remains pending until the script timeout. See the Selenium JavaScript API.
const result = await driver.executeAsyncScript(function () {
const done = arguments[arguments.length - 1];
fetch('/api/status')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
})
.then(data => done({ status: data.status }))
.catch(error => done({ error: String(error) }));
});
console.log(result);
This page function cannot rely on lexical variables from the Node.js test process: Selenium serializes the function for execution in the browser context. Pass values as script arguments when needed, and read them from the page function’s arguments before the final callback argument.
const endpoint = '/api/status';
const result = await driver.executeAsyncScript(function (path) {
const done = arguments[arguments.length - 1];
fetch(path)
.then(response => response.json())
.then(data => done(data))
.catch(error => done({ error: String(error) }));
}, endpoint);
Do not assume that returning a Promise from the page function makes WebDriver wait for it. The documented completion mechanism for this API is invoking Selenium’s injected callback. For ordinary UI readiness, a condition wait is often easier to read and maintain than injecting a fetch or arbitrary page script.
executeScript versus executeAsyncScript
| Need | API | How it completes |
|---|---|---|
| Read or change page state with synchronous page code | executeScript() |
The script returns its value when it finishes. |
| Run asynchronous page work and return a value | executeAsyncScript() |
Page code calls Selenium’s injected callback. |
| Sequence WebDriver commands in JavaScript test code | async function and await |
The WebDriver promise resolves or rejects. |
| Wait until a UI condition is met | driver.wait(condition, timeout) |
The condition becomes truthy, or the wait times out. |
4. Set timeouts deliberately
Keep asynchronous script execution bounded. The JavaScript WebDriver implementation documentation describes a 30,000 millisecond default script timeout, but defaults can vary by binding or version. Set the timeout explicitly when page-side work needs a different limit, and check the documentation for your installed Selenium version.
await driver.manage().setTimeouts({ script: 15_000 });
const value = await driver.executeAsyncScript(function () {
const done = arguments[arguments.length - 1];
setTimeout(() => done('finished'), 500);
});
The script timeout applies to asynchronous scripts executed in the page. It is distinct from the timeout passed to driver.wait(), which bounds a condition wait. Choose each limit based on the operation and keep it finite so a stalled page does not hang the test indefinitely.
5. Handle failures and clean up reliably
Use try/finally around the browser session. Add error context to waits so failures tell you which condition was missing. Catch errors at the test-runner boundary to report a failing process status, as in the setup example.
async function runCheck() {
const driver = await new Builder().forBrowser('chrome').build();
try {
await driver.get('https://example.test');
await driver.wait(
until.elementLocated(By.css('.account-summary')),
8_000,
'Account summary did not load'
);
} finally {
await driver.quit();
}
}
If browser creation itself fails, the try block has not started and there may be no session to close. If session shutdown fails after a test error, preserve the original failure in your runner’s reporting so cleanup errors do not obscure the cause.
6. Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| Next command runs before navigation or click finishes | A promise-returning WebDriver command was not awaited. | Add await to the command and to helpers that return its promise. |
await causes a syntax error |
The code is outside an async function, or the project’s top-level await/module setup does not support it. |
Move the code into an async function or configure the project module format appropriately. |
executeAsyncScript times out |
The page function never called its final callback, or the operation took longer than the configured script timeout. | Call the callback on both success and failure paths; set a suitable finite script timeout. |
| Page script reports that a Node variable is undefined | The serialized browser function cannot see the Node.js lexical scope. | Pass the value as an argument to executeAsyncScript. |
| An element is found but cannot be clicked | Located does not mean visible, enabled, or unobstructed. | Wait for the needed visible/enabled state and investigate overlays or layout changes. |
| Test is slow or still flaky after a sleep | The fixed duration neither identifies readiness nor adapts to the actual page condition. | Wait for a specific UI condition with a bounded timeout. |
| Python or Java timeout example does not work in JS | Timeout APIs are binding-specific. | Use JavaScript binding syntax here; consult the matching binding documentation for other languages. |
7. Performance, reliability, and cost
Condition waits usually avoid unnecessary idle time because they can finish when the condition becomes true, while a fixed sleep always consumes its full duration. They also make the test’s readiness requirement explicit. Neither approach guarantees that an application is correct; the condition must represent the state the test actually needs.
Keep timeouts bounded and specific. Very short limits can fail on slow environments; overly long limits delay feedback when something is broken. Use the smallest meaningful condition and avoid repeatedly querying unrelated page state. Always close sessions in cleanup so browser processes and remote sessions are not left running after a failed assertion.
There is no Selenium-specific price implied by the code examples. The cost of a Selenium run depends on where and how the browser session is hosted. If your task is simply to obtain a rendered page screenshot rather than interact with the browser, an API can remove the need to provision and maintain browser automation infrastructure.
8. Or skip the browser setup
For a screenshot without managing Selenium, ScreenshotNeo returns a rendered image or PDF from one GET request. The API parameters and options are documented at ScreenshotNeo’s 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}`);
ScreenshotNeo accepts cookie and consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers identify the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000 screenshots.
Sign up for ScreenshotNeo and get 1,000 screenshots a month free, with no card.
9. FAQ
Does await make the JavaScript inside the page asynchronous?
No. It waits for a promise in the Node.js test process. Page-side asynchronous execution uses executeAsyncScript() and its completion callback.
Should I use executeAsyncScript for every wait?
No. Prefer a condition-based WebDriver wait when the test needs to observe application state. Use asynchronous page scripts when page-context work must return a value.
Can Selenium JavaScript executeAsyncScript wait for a returned Promise?
Use the injected callback as the completion signal. Do not rely on a returned page Promise to complete the WebDriver command.
Can I use this JavaScript syntax in Selenium Python?
No. Python’s execute_async_script has a similar callback contract, but Python tests do not use JavaScript await syntax. Follow the API for the binding and framework you are using.


