PRACTICAL GUIDE / Selenium JavaScript async await

Selenium JavaScript Async/Await Patterns That Preserve Command Order

Preserve Selenium JavaScript command order with explicit async/await, sequential iteration, failure-safe helpers, and awaited WebDriver cleanup in Node tests.

By The Testing AcademyUpdated July 11, 20269 min read
All field guides
In this guide12 sections
  1. Build One Explicit Dependency Chain
  2. Write the Happy Path as Serialized Browser Work
  3. Understand Thenables Without Depending on Cleverness
  4. Return the Test Promise to the Runner
  5. Replace async forEach With Sequential Iteration
  6. Reserve Promise.all for Real Independence
  7. Preserve the Primary Error When Quit Also Fails
  8. Keep Page Objects Asynchronous at Their Edges
  9. Diagnose Ordering Failures From the First Missing Boundary
  10. Balance Readability and Throughput
  11. Audit Every Async Boundary
  12. End With Causal Code

What you will learn

  • Build One Explicit Dependency Chain
  • Write the Happy Path as Serialized Browser Work
  • Understand Thenables Without Depending on Cleverness
  • Return the Test Promise to the Runner

Selenium JavaScript code is reliable when command dependencies are visible in the source. Navigation must finish before a page-specific lookup, lookup must produce the intended element before an interaction, and the final assertion must settle before the session is released. Missing one await breaks that chain even if a fast local run happens to pass.

Treat every WebDriver call as asynchronous work with an owner and a result. Await dependent commands in order, return promises from helpers, use sequential loops for one browsing context, and await cleanup in finally. Parallel syntax is not useful when the browser state itself is sequential.

Build One Explicit Dependency Chain

The Selenium JavaScript API uses promises and thenable objects throughout the binding. Builder.build(), navigation, element interactions, waits, script execution, screenshots, and quit() all cross asynchronous boundaries. JavaScript continues after a call until an await, return, or promise chain makes completion part of the control flow.

Command order is therefore a program property, not a comment or naming convention. A line appearing first does not mean its promise settled first. If the next line needs the side effect or value, await the prior line at that boundary.

Animated field map

Awaited WebDriver Command Chain

An async test waits for each remote result before issuing a dependent command and releases the session through an awaited finally path.

  1. 01 / async test

    Async Test

    The runner receives and awaits the promise returned by the test function.

  2. 02 / awaited command

    Awaited WebDriver Command

    Navigation or interaction remains pending until its promise settles.

  3. 03 / remote response

    Remote Response

    The binding resolves a value or throws the protocol error at the await point.

  4. 04 / dependent command

    Dependent Command

    The next lookup, action, or assertion starts only after required state exists.

  5. 05 / finally quit

    Finally Block Quit

    An awaited quit releases the session after success or failure.

Write the Happy Path as Serialized Browser Work

A test should expose the browser's causal order without relying on hidden promise behavior. Await the builder, navigation, each dependent element operation, the condition that marks completion, and values used in assertions.

JavaScript
const assert = require('node:assert/strict');
const { Builder, Browser, By, until } = require('selenium-webdriver');

async function loginAs(user) {
  const driver = await new Builder().forBrowser(Browser.CHROME).build();

  try {
    await driver.get('https://example.test/login');

    const email = await driver.findElement(By.name('email'));
    await email.sendKeys(user.email);

    const password = await driver.findElement(By.name('password'));
    await password.sendKeys(user.password);

    const submit = await driver.findElement(By.css('button[type="submit"]'));
    await submit.click();

    await driver.wait(until.urlContains('/dashboard'), 10_000);
    const heading = await driver.findElement(By.css('main h1'));
    assert.equal(await heading.getText(), 'Dashboard');
  } finally {
    await driver.quit();
  }
}

The timeout is a suite policy input in real code, not a universal value. The ordering is the lesson: each command that changes or reads shared browsing-context state settles before the next dependent operation begins. The official driver session documentation also places creation and quitting around the commands that use the session.

Understand Thenables Without Depending on Cleverness

findElement can return a WebElementPromise, which is both a WebElement-facing proxy and a thenable. This permits concise expressions such as await driver.findElement(locator).click(). That can be readable for one operation, but awaiting the element separately gives a clearer failure boundary when several actions use it.

JavaScript
async function readOrderTotal(driver) {
  const total = await driver.findElement(By.css('[data-testid="order-total"]'));
  const visible = await total.isDisplayed();
  assert.equal(visible, true);
  return await total.getText();
}

Do not store unresolved thenables in page-object fields during construction. Constructors cannot be awaited, and later methods may fail far from the lookup that created the promise. Store locators, then resolve elements inside async methods. This also avoids holding stale element references across page changes.

An async helper should either return a plain value obtained through awaits or return the promise that its caller must await. A helper that starts a command and returns undefined has detached the work from the test lifecycle.

Return the Test Promise to the Runner

Most Node test runners understand an async test function because it returns a promise. The runner can mark the test complete only after that promise settles. If a wrapper calls an async function without returning or awaiting it, the runner may report success while browser work continues in the background.

JavaScript
describe('account access', function () {
  it('opens the dashboard', async function () {
    await loginAs({
      email: process.env.TEST_EMAIL,
      password: process.env.TEST_PASSWORD,
    });
  });
});

Avoid mixing callback-style completion such as done with an async test. Two completion channels create ambiguous ownership: the callback can fire before a promise rejects, or the promise can settle while the callback never runs. Pick the runner's promise contract and use it throughout helpers and hooks.

At process level, do not suppress unhandledRejection to keep CI green. An unhandled rejection is evidence that asynchronous work escaped its owner. Fix the missing return or await and let the test fail at the responsible boundary.

Replace async forEach With Sequential Iteration

Array forEach does not await promises returned by its callback. It invokes all callbacks and returns immediately. That is especially dangerous for a single WebDriver because every callback may navigate or mutate the same browsing context.

JavaScript
// Incorrect: the outer function does not await callback promises.
labels.forEach(async (label) => {
  await driver.findElement(By.linkText(label)).click();
});

// Correct for dependent operations in one browser.
for (const label of labels) {
  const link = await driver.findElement(By.linkText(label));
  await link.click();
  await driver.navigate().back();
}

Other array helpers have similar traps. map returns an array of promises, which is useful only when the caller deliberately awaits them. filter expects a synchronous truth value, so an async predicate does not filter by its eventual boolean. For browser steps, a plain for...of loop is usually the honest representation.

Reserve Promise.all for Real Independence

Promise.all is not a general speed switch. Two commands against the same page can depend on focus, navigation, DOM replacement, window selection, or alert state even when the JavaScript expressions appear unrelated. Starting them together removes the explicit order that makes failures reproducible.

Parallel promise aggregation is appropriate for independent non-browser work, such as reading several fixture files before session creation. It can also coordinate separate WebDriver instances when each task owns its own session and data. The unit of parallelism is the owned session, not arbitrary commands inside one session.

JavaScript
async function runAcrossBrowsers(cases) {
  const results = await Promise.all(
    cases.map(async ({ browser, url }) => {
      const driver = await new Builder().forBrowser(browser).build();
      try {
        await driver.get(url);
        return { browser, title: await driver.getTitle() };
      } finally {
        await driver.quit();
      }
    }),
  );

  return results;
}

This pattern creates a complete lifecycle inside each mapped async function. Bound the number of cases to local or Grid capacity; Promise.all otherwise attempts every session creation at once.

Preserve the Primary Error When Quit Also Fails

A simple finally { await driver.quit(); } guarantees an attempt, but a rejected quit can replace an earlier assertion error. Production helpers should retain the primary failure and report cleanup failure as secondary evidence. If the test itself succeeded, a failed quit should still fail the lifecycle because the session may have leaked.

JavaScript
async function withDriver(browser, useDriver) {
  let driver;
  let primaryError;

  try {
    driver = await new Builder().forBrowser(browser).build();
    return await useDriver(driver);
  } catch (error) {
    primaryError = error;
    throw error;
  } finally {
    if (driver) {
      try {
        await driver.quit();
      } catch (quitError) {
        if (primaryError) {
          console.error('WebDriver quit failed after test failure', quitError);
        } else {
          throw quitError;
        }
      }
    }
  }
}

The conditional matters because construction may fail before driver is assigned. Each additional resource should have its own guarded release so one cleanup error does not skip the rest.

Keep Page Objects Asynchronous at Their Edges

Page objects should store driver and locators, then expose async actions and observations. Do not hide an unawaited command behind a method with a synchronous-looking name. Callers should see await checkout.submit() and know when the page transition has completed.

JavaScript
class CheckoutPage {
  constructor(driver) {
    this.driver = driver;
    this.placeOrder = By.css('[data-testid="place-order"]');
    this.confirmation = By.css('[data-testid="confirmation-number"]');
  }

  async submit() {
    const button = await this.driver.findElement(this.placeOrder);
    await button.click();
    const result = await this.driver.wait(
      until.elementLocated(this.confirmation),
      10_000,
    );
    return await result.getText();
  }
}

Return a domain value after the page reaches a defined state. That is stronger than returning immediately after click(), because the caller otherwise has to guess which wait proves completion.

Diagnose Ordering Failures From the First Missing Boundary

If a test finishes before the browser action, inspect whether the runner received the async promise. If actions happen in an unexpected sequence, search for missing awaits, async callbacks passed to forEach, detached helper calls, and Promise.all against one driver. If teardown races with a command, a helper likely returned before its WebDriver promise settled.

Errors that appear as unhandled rejections usually identify work that escaped the test promise. Errors at quit() can indicate the session was already closed, the node disappeared, or another code path released shared state. Do not add arbitrary sleeps; they change timing without restoring ownership.

Balance Readability and Throughput

Awaiting dependent commands can look verbose, but it documents protocol boundaries and gives stack traces useful source lines. Dense chains are shorter but can obscure which lookup or action failed. Choose named intermediate values when they carry domain meaning or improve diagnostics.

Parallel browsers can improve throughput at the cost of more Grid slots, accounts, memory, and artifact coordination. Parallel commands within one browser rarely provide the same benefit because they still compete over one stateful browsing context. Scale by complete session lifecycles.

Audit Every Async Boundary

  • The test runner receives the promise returned by every async test and hook.
  • Every dependent WebDriver command or result is explicitly awaited.
  • Page objects store locators rather than unresolved element promises.
  • Helpers return awaited domain values or promises their callers await.
  • Sequential browser steps use for...of, not async forEach.
  • Promise.all is limited to independent work or separate owned drivers.
  • Session creation and quit() both belong to one try/finally lifecycle.
  • Quit is awaited, and cleanup failure does not erase a primary test failure.
  • Parallel session count is bounded to browser, Grid, and test-data capacity.

End With Causal Code

Reliable Selenium JavaScript reads like the browser's actual dependency graph. Each awaited response authorizes the next dependent command, and one final awaited cleanup closes the session. Make that causality explicit and async failures stop looking random: they appear where ownership or order was genuinely broken.

// LIVE COURSE / THE TESTING ACADEMY

Playwright Automation Mastery

Go beyond Selenium. Master Playwright with JS/TS in 90 days.

From the instructor behind this guide.

Playwright jobs are growing 8x faster than Selenium. 90 days / 75+ live hrs / Tue-Thu-Sat 7 AM IST.

Code PROMODE / 10% offJoin the batch

The Testing Academy editorial desk

Practical QA guidance built around test evidence, production tradeoffs, and interview-ready explanations.

Published July 11, 2026 / Reviewed July 11, 2026

PRIMARY REFERENCES

Verify the details at the source

QABattle guides are practical explanations. Product behavior, standards, and APIs can change, so use these primary references for the canonical details.

  1. 01
    Selenium documentation

    Selenium Project

    Canonical WebDriver, Grid, waits, element, and browser automation guidance.

  2. 02
    WebDriver standard

    W3C

    The browser automation protocol specification behind WebDriver implementations.

FAQ / QUICK ANSWERS

Questions testers ask

Do Selenium JavaScript commands need to be awaited individually?

Await every command whose completion or result is required by the next step. Explicit awaits make navigation, element lookup, interaction, assertion, and cleanup order visible.

Why is async forEach unsafe for Selenium command sequences?

forEach ignores the promises returned by its callback. The outer function can continue or finish while WebDriver work is still pending; use for...of for sequential steps.

Can Promise.all run several commands against one WebDriver faster?

It is a poor fit for commands that share one browsing context because their effects and observations are dependent. Use it for truly independent work or separate owned drivers.

Should driver.quit be awaited in a finally block?

Yes. Awaiting quit keeps the test process alive until the remote session is released. Guard quit errors so they do not hide an earlier test failure.

What is a WebElementPromise in the JavaScript binding?

It is a thenable returned by element lookup that can participate in promise chains. Awaiting it gives an ordinary WebElement and makes failures occur at a clear source line.