For AI agents: a documentation index is available at /llms.txt
Skip to main content

Waiting for things in BAP

Most timeouts in automation come from waiting for the wrong thing. BAP has five explicit waits plus several implicit ones, and picking correctly is usually the difference between a flaky script and a reliable one. This guide covers each wait, what it actually observes, and which waits you don't need to write.

Prerequisites

Pick a wait

You're waiting forUse
An element to appear, or disappearwaitForSelector()
A navigation you just triggered to finishwaitForNavigation()
The page to stop making requestswaitForNetworkIdle()
Some JavaScript condition to become truewaitForFunction()
A specific request or responsewaitForRequest() / waitForResponse()
A page event to fire oncewaitForEvent()
A fixed amount of timewaitForTimeout(), and almost never

Every wait accepts a timeout that overrides the 30-second default for that call. When one fires, you get a TimeoutError, covered in error handling.

Waits you don't need to write

Interaction and extraction methods already wait for their selector. click(), type(), text(), html(), and mapSelector() all default to wait: true, which means an explicit waitForSelector() before them is redundant:

// Redundant: click() already waits for the selector.
await page.waitForSelector("#submit");
await page.click("#submit");

// Enough on its own.
await page.click("#submit");

Reach for an explicit waitForSelector() when you need to wait for something you aren't about to act on, such as a loading spinner disappearing or a result count rendering before you extract.

Wait for an element

waitForSelector() takes visible and hidden booleans rather than Playwright's state enum. hidden is the one worth knowing: it's how you wait for a spinner or overlay to go away.

// Present in the DOM.
await page.waitForSelector(".results");

// Present and actually rendered.
await page.waitForSelector(".results", { visible: true });

// Gone, which is how you wait out a loading overlay.
await page.waitForSelector(".spinner", { hidden: true, timeout: 15_000 });

Wait for navigation

goto() waits for the page to load on its own. waitForNavigation() is for navigations you trigger some other way, usually a click, and it must be in flight before the navigation completes or you'll wait for one that already happened. In TypeScript that means starting the wait without awaiting it, then awaiting after the click. The Python sync API can't express that ordering, so wait for an element unique to the destination instead, which sidesteps the race in both languages.

Both take waitUntil, which decides what "loaded" means: commit, load, domContentLoaded, firstContentfulPaint, firstMeaningfulPaint, interactiveTime, or networkIdle. domContentLoaded returns soonest and is enough when your target is in the initial HTML; networkIdle waits longest and suits pages that assemble themselves with XHR.

// Start the wait first, so a fast navigation can't finish before it begins.
const navigated = page.waitForNavigation({ waitUntil: "networkIdle" });
await page.click("#login");
await navigated;

console.log(await page.url());

// Cheaper when the content you want is in the first response.
await page.goto("https://example.com", { waitUntil: "domContentLoaded" });

Wait for the network to settle

waitForNetworkIdle() returns once the page stops making requests. Two options shape it: idleTime (default 500 milliseconds) is how long quiet counts as idle, and concurrency (default 0) is how many in-flight requests you'll tolerate while still calling it idle.

Raise concurrency on pages that keep a long-poll or analytics beacon open forever, since with the default those pages never reach idle and you wait out the full timeout instead.

// Strict: no requests in flight for half a second.
await page.waitForNetworkIdle();

// Tolerant: ignore up to two long-lived connections.
await page.waitForNetworkIdle({ concurrency: 2, idleTime: 1000 });

Wait for a JavaScript condition

waitForFunction() evaluates an expression in the page until it returns something truthy. It's the escape hatch for conditions no selector expresses, such as a global that a third-party script sets, or a computed count.

Polling defaults to every 100 milliseconds. pollingType swaps that for browser-driven modes: raf re-evaluates each animation frame, and mutation re-evaluates on DOM changes, which is cheaper than fast interval polling on a busy page.

// Wait for a third-party script to signal readiness.
await page.waitForFunction("window.appReady === true");

// Re-evaluate on DOM changes instead of on a timer.
await page.waitForFunction(
"document.querySelectorAll('.row').length > 20",
{ pollingType: "mutation", timeout: 20_000 },
);

Pass completeOnPromiseResolution: true when your expression returns a promise and you want the wait to end once it settles, whatever it resolves to.

Wait for an event

waitForEvent() resolves the next time a page event fires, which is the one-shot counterpart to the listeners in page events. watchEvent() starts watching without blocking, so you can trigger the action first and collect the event afterward.

// Block until the next console message.
const event = await page.waitForEvent("console", { timeout: 10_000 });

Why not a fixed sleep

waitForTimeout() exists and takes a duration in milliseconds, but it's the wrong tool nearly every time: too short and the script is flaky, too long and every run pays for the worst case. Worse, it holds a billed session open doing nothing. Wait on the condition you actually care about instead. The honest use for a fixed sleep is pacing something deliberately, such as spacing interactions on a site that rate-limits by timing.

FAQ & Troubleshooting

My waitForNetworkIdle() always times out

The page keeps a connection open, usually analytics, a long-poll, or a websocket, so in-flight requests never reach zero. Raise concurrency to tolerate those, or wait for a selector that proves your content arrived instead.

waitForNavigation() resolved immediately, or timed out for no reason

It has to be waiting before the navigation completes. If you await the click first and then call it, a fast navigation is already done and you're waiting for the next one. In TypeScript, start the wait without awaiting, click, then await it. In Python's sync API, where that ordering isn't expressible, wait for an element unique to the destination instead.

Should I use waitForSelector() before every click?

No. click(), type(), and the extraction methods default to wait: true and already wait for their selector. Add an explicit wait only for things you aren't about to act on.

waitForFunction() never resolves even though the condition is true in DevTools

The expression runs in the page, so it can't see variables from your script's scope, and it must return a value rather than only having a side effect. Check that it's an expression, not a statement, and that the global you're testing exists on window at that point.

Next steps

Was this page helpful?