Intermediate Chapter 8 · 13 min read

Handling Waits & Timeouts

Understand Playwright's auto-waiting mechanism and learn when to use explicit waits for network, state, and custom conditions.

Auto-Waiting Explained

Playwright's auto-waiting engine checks a checklist of conditions before executing any action: the element must be attached to the DOM, visible, stable (not animating), enabled, and — for inputs — not read-only. This happens automatically for every click(), fill(), or check() call. You do not need to sprinkle waitForSelector before every interaction the way you would in older frameworks.

Tip: If a test is timing out, open the Playwright Trace Viewer (npx playwright show-trace trace.zip) and look at the action timeline to see exactly which auto-wait condition was not met.

Explicit Waits

Sometimes you need to wait for a specific network response before proceeding — for example, waiting for a POST request to complete before asserting that a record appeared in a table. Use page.waitForResponse() with a URL pattern, or page.waitForRequest() for the outgoing side. These methods return promises that resolve when the matching request is found, so you can await them alongside the triggering action using Promise.all().

Load States and Custom Conditions

page.waitForLoadState() lets you wait for 'load', 'domcontentloaded', or 'networkidle'. For custom conditions — such as waiting for a JavaScript variable to be set — use page.waitForFunction() which polls the page context until the provided expression returns a truthy value. Always prefer semantic waits over arbitrary sleep delays.

MethodWhen to use
Auto-wait (implicit)Before every action — always active
waitForResponse(url)After clicking something that triggers an API call
waitForLoadState('networkidle')After SPA navigation with lazy-loaded content
waitForFunction(expr)Custom JS condition in the page context
locator.waitFor()Wait for a specific element state
waits.spec.js
import { test, expect } from '@playwright/test';

test('wait for API response after form submit', async ({ page }) => {
  await page.goto('/orders/new');
  await page.getByLabel('Product').fill('Widget A');
  await page.getByLabel('Quantity').fill('3');

  // Wait for both the click AND the response simultaneously
  const [response] = await Promise.all([
    page.waitForResponse(resp =>
      resp.url().includes('/api/orders') && resp.status() === 201
    ),
    page.getByRole('button', { name: 'Place Order' }).click(),
  ]);

  const body = await response.json();
  expect(body.orderId).toBeDefined();

  // Wait for element to reach a specific state
  await page.getByTestId('order-status').waitFor({ state: 'visible' });

  // Custom JS condition — wait until total updates
  await page.waitForFunction(() =>
    document.querySelector('[data-testid="total"]')?.textContent !== '$0.00'
  );

  // Load state after SPA navigation
  await page.getByRole('link', { name: 'History' }).click();
  await page.waitForLoadState('networkidle');
  await expect(page.getByRole('table')).toBeVisible();
});
WaitsTest.java
import com.microsoft.playwright.*;
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;

class WaitsTest {
  void waitForApiResponse(Page page) {
    page.navigate("/orders/new");
    page.getByLabel("Product").fill("Widget A");
    page.getByLabel("Quantity").fill("3");

    // Start waiting BEFORE clicking
    Response response = page.waitForResponse(
      resp -> resp.url().contains("/api/orders") &&
               resp.status() == 201,
      () -> page.getByRole(AriaRole.BUTTON,
              new Page.GetByRoleOptions().setName("Place Order"))
            .click()
    );

    // Wait for element visibility
    page.getByTestId("order-status")
        .waitFor(new Locator.WaitForOptions()
            .setState(WaitForSelectorState.VISIBLE));

    // Network idle after navigation
    page.getByRole(AriaRole.LINK,
        new Page.GetByRoleOptions().setName("History")).click();
    page.waitForLoadState(LoadState.NETWORKIDLE);
    assertThat(page.getByRole(AriaRole.TABLE)).isVisible();
  }
}
test_waits.py
from playwright.sync_api import Page, expect

def test_wait_for_api_response(page: Page):
    page.goto("/orders/new")
    page.get_by_label("Product").fill("Widget A")
    page.get_by_label("Quantity").fill("3")

    # Use context manager — starts waiting BEFORE clicking
    with page.expect_response(
        lambda r: "/api/orders" in r.url() and r.status == 201
    ) as resp_info:
        page.get_by_role("button", name="Place Order").click()

    response = resp_info.value
    body = response.json()
    assert "orderId" in body

    # Wait for element state
    page.get_by_test_id("order-status").wait_for(state="visible")

    # Wait for custom JS condition
    page.wait_for_function(
        "document.querySelector('[data-testid="total"]')?.textContent !== '$0.00'"
    )

    # Network idle after SPA navigation
    page.get_by_role("link", name="History").click()
    page.wait_for_load_state("networkidle")
    expect(page.get_by_role("table")).to_be_visible()

Playwright Intermediate Handling Waits & Timeouts

Written by PV

© 2026 All Rights Reserved