Flaky tests are almost always waiting problems. Playwright solves most of them with auto-waiting, but you still need to know when and how to wait for elements, pages, URLs and API responses. This guide explains every Playwright wait method with TypeScript examples and clear advice on which one to use.
๐ What You’ll Learn: auto-waiting and actionability checks, web-first assertions, locator.waitFor, waitForLoadState, waitForURL, waitForResponse, why to avoid waitForTimeout and networkidle, waitForFunction, expect.poll, toPass, timeouts and troubleshooting.
๐ค Auto-Waiting: Why You Rarely Need Manual Waits
The biggest difference between Playwright and older tools is auto-waiting. Before every action, Playwright waits until the element is ready, so you do not write sleeps or explicit waits for normal clicks and typing.
Before click(), for example, Playwright checks that the element is:
- Attached to the page (it exists in the DOM)
- Visible (not hidden, has a size)
- Stable (not moving or animating)
- Receives events (not covered by another element such as a loader)
- Enabled (not disabled)
fill() additionally waits until the field is editable. If the checks do not pass within the timeout, the action fails with a clear error explaining which check failed.
// No waits needed: Playwright waits for the button to be ready
await page.getByRole('button', { name: 'Login' }).click();
await page.getByLabel('Email').fill('qa@sqatools.in');
๐ก Tip: Most “wait” problems in Playwright disappear once you use locators (page.getByRole, page.locator) and web-first assertions (await expect(...)). Reach for the methods below only for special cases.
โ Web-First Assertions: The Best Way to Wait
Assertions such as toBeVisible(), toHaveText() and toHaveURL() retry automatically until the condition is true or the timeout (5 seconds by default) expires. They are the cleanest way to wait for a result.
import { test, expect } from '@playwright/test';
test('order confirmation appears', async ({ page }) => {
await page.goto('https://www.saucedemo.com/');
// ...log in and place an order...
// Waits up to 5 s (default) for the element and the text
await expect(page.getByText('Thank you for your order!')).toBeVisible();
// Custom timeout for a slow step
await expect(page.locator('.complete-header')).toHaveText('Thank you for your order!', { timeout: 15_000 });
// Wait for an element to disappear
await expect(page.locator('.loading-spinner')).toBeHidden();
});
โ ๏ธ Common mistake: Do not write expect(await locator.isVisible()).toBe(true). isVisible() checks once and does not wait. Use await expect(locator).toBeVisible() instead.
๐๏ธ Wait for an Element to Be Visible: locator.waitFor()
When you need to wait for an element without asserting (for example inside a helper), use locator.waitFor() with a state.
const toast = page.locator('.toast-message');
await toast.waitFor(); // default state: 'visible'
await toast.waitFor({ state: 'hidden' }); // wait until it disappears
await toast.waitFor({ state: 'attached' }); // exists in the DOM (may be hidden)
await toast.waitFor({ state: 'detached', timeout: 10_000 }); // removed from the DOM
๐ Wait for the Page to Load: waitForLoadState()
page.goto() already waits for the load event by default. Use waitForLoadState() when the page loads after an action, such as a click that triggers a navigation, or when you need a different state.
| State | Meaning |
|---|---|
load | The load event fired (all resources such as images loaded). Default for goto. |
domcontentloaded | HTML parsed; images and styles may still be loading. |
networkidle | No network requests for at least 500 ms. Discouraged. |
await page.goto('https://sqatools.in', { waitUntil: 'domcontentloaded' });
await page.getByRole('link', { name: 'Blog' }).click();
await page.waitForLoadState('domcontentloaded');
โ ๏ธ Common mistake: networkidle is a common way to “wait for network idle”, but Playwright discourages it: pages with analytics, polling or live chat may never become idle, which makes tests slow or flaky. Wait for the specific element or response you care about instead.
๐ Wait for a URL: waitForURL()
After a click that navigates, wait for the new URL. waitForNavigation() is deprecated; use waitForURL() or the toHaveURL() assertion.
await page.getByRole('button', { name: 'Login' }).click();
await page.waitForURL('**/inventory.html'); // glob pattern
await page.waitForURL(/inventory/); // regular expression
// Or as an assertion (also retries)
await expect(page).toHaveURL(/inventory/);
๐ก Wait for an API Response: waitForResponse()
Many modern pages load data with API calls. Wait for the exact response, then check its data. Start waiting before the action that triggers the request, otherwise you can miss a fast response.
// 1. Start listening first (no await yet)
const responsePromise = page.waitForResponse(resp =>
resp.url().includes('/api/products') && resp.status() === 200
);
// 2. Trigger the request
await page.getByRole('button', { name: 'Search' }).click();
// 3. Now await the response
const response = await responsePromise;
const data = await response.json();
expect(data.items.length).toBeGreaterThan(0);
The same pattern works for page.waitForRequest() and other events, such as page.waitForEvent('download') and page.waitForEvent('popup').
โฑ๏ธ Wait for 5 Seconds: waitForTimeout() (and Why to Avoid It)
You can pause a test for a fixed time:
await page.waitForTimeout(5000); // waits 5 seconds
This is a hard wait. It makes tests slower when the app is fast and still flaky when the app is slower than expected. Playwright’s documentation says it should be used only for debugging. Replace it with a condition:
| Instead of | Use |
|---|---|
| waitForTimeout after a click | expect(locator).toBeVisible() |
| waitForTimeout for a spinner | expect(spinner).toBeHidden() |
| waitForTimeout for data to load | page.waitForResponse(…) |
| waitForTimeout for navigation | page.waitForURL(…) |
๐ Custom Conditions: waitForFunction, expect.poll and toPass
page.waitForFunction()
Runs a function in the browser repeatedly until it returns a truthy value.
await page.waitForFunction(() => document.querySelectorAll('.inventory_item').length >= 6);
expect.poll()
Retries any Node.js function, for example an API call, until the assertion passes.
await expect.poll(async () => {
const res = await page.request.get('https://api.example.com/jobs/42');
return (await res.json()).status;
}, { timeout: 30_000, intervals: [1_000, 2_000, 5_000] }).toBe('COMPLETED');
expect(…).toPass()
Retries a whole block of steps until all of them pass.
await expect(async () => {
await page.getByRole('button', { name: 'Refresh' }).click();
await expect(page.getByText('Report ready')).toBeVisible({ timeout: 1_000 });
}).toPass({ timeout: 20_000 });
โ๏ธ Timeouts: Defaults and How to Change Them
| Timeout | Default | Where to change |
|---|---|---|
| Test timeout | 30 seconds | timeout in config, test.setTimeout() |
| Assertion (expect) timeout | 5 seconds | expect: { timeout } in config, or per assertion |
| Action timeout (click, fill) | No limit (bounded by the test timeout) | use: { actionTimeout } |
| Navigation timeout (goto, waitForURL) | No limit (bounded by the test timeout) | use: { navigationTimeout } |
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
timeout: 60_000, // each test may run 60 s
expect: { timeout: 10_000 }, // each assertion waits up to 10 s
use: {
actionTimeout: 15_000, // each click/fill waits up to 15 s
navigationTimeout: 30_000, // each goto waits up to 30 s
},
});
// For one slow test
test('generate yearly report', async ({ page }) => {
test.slow(); // triples the timeout
// or: test.setTimeout(120_000);
});
Learn more about config in maintaining multiple config files.
๐งน Legacy Methods You May See in Old Code
page.waitForSelector(): still works, but the docs recommend locators withwaitFor()or assertions.page.waitForNavigation(): deprecated; usewaitForURL().page.waitForTimeout(): only for debugging.
๐ Troubleshooting Timeout Errors
- “Timeout 30000ms exceeded” on an action: the element never became actionable. Run with
npx playwright test --uior open the trace to see whether it was hidden, covered or disabled. - Element is covered by another element: wait for the overlay or spinner to disappear with
expect(overlay).toBeHidden(). - Strict mode violation: your locator matched several elements. Make it more specific; see Playwright locators.
- Test passes locally but fails in CI: CI machines are slower. Prefer condition-based waits and enable
trace: 'on-first-retry'to inspect failures.
โ Best Practices
- Trust auto-waiting; do not add waits before normal actions.
- Use web-first assertions as your main waiting tool.
- Start
waitForResponseandwaitForEventpromises before the triggering action. - Avoid
waitForTimeoutandnetworkidlein real tests. - Increase timeouts for specific slow steps instead of globally.
โ FAQ
๐ค How do I wait for an element to be visible in Playwright?
Use await expect(locator).toBeVisible() in tests, or await locator.waitFor({ state: 'visible' }) in helper code. Both wait automatically up to the timeout.
๐ค How do I wait for a page to load in Playwright?
page.goto() waits for the load event by default. After a click, use page.waitForURL() or page.waitForLoadState(), or simply assert on an element of the new page.
๐ค How do I wait for 5 seconds in Playwright?
await page.waitForTimeout(5000). Use it only while debugging; in real tests wait for a condition instead.
๐ค Does Playwright have implicit waits like Selenium?
Not as a setting. Playwright’s auto-waiting is applied automatically to every action and web-first assertion, which replaces implicit waits. Compare with Selenium waits.
๐ค What is the default timeout in Playwright?
30 seconds per test and 5 seconds per expect assertion. Actions and navigations have no separate limit by default, so they are bounded by the test timeout.
๐ฏ Next Steps
- ๐ญ Playwright Assertions: Complete Guide
- ๐ญ Handle Alerts, Popups, Dropdowns, iFrames and Tabs
- ๐ญ Playwright vs Selenium vs Cypress
- ๐ญ Playwright with TypeScript Training Notes
๐ฌ Watch the full Playwright tutorial series on YouTube!
Subscribe to SqaToolsByDY • Practice on sqatools.in
9 thoughts on “Playwright Waits Explained: Auto-Waiting, waitFor, waitForLoadState, waitForResponse and Timeouts”