How to Wait for a Page Condition in Puppeteer
Choose the right Puppeteer wait for selectors, visibility, app state, actions, or navigation—with runnable examples, timeout guidance, and fixes for common failures.
Use the wait that matches the condition your code needs: page.waitForSelector() for DOM presence or visibility, page.waitForFunction() for a custom application state, a locator for readiness to interact, and page.waitForNavigation() when an action navigates.
There is no single best wait for every page. A selector appearing does not prove it is clickable, a client-side update may not navigate, and waiting for navigation after clicking can miss the event. The examples below use Puppeteer’s documented APIs; check the documentation for the version installed in your project because APIs can change.
1. Choose the wait that matches the condition
| What must become true? | Use | What it establishes |
|---|---|---|
| A matching element enters the DOM | page.waitForSelector(selector) |
Selector presence; resolves immediately if already present. |
| An element becomes visible or hidden | page.waitForSelector(selector, {visible: true}) or {hidden: true} |
DOM presence plus the documented visibility check, or absence/hidden state. |
| Application-specific state is ready | page.waitForFunction(predicate) |
A truthy value returned by a callback running in the page. |
| An element is ready for an interaction | page.locator(selector).click() |
The action retries its documented preconditions, such as visibility, enabled state, viewport placement, and a stable bounding box for clicking. |
| A click causes navigation | page.waitForNavigation() started before the click |
Navigation or reload completion according to the configured lifecycle condition. |
Selector waits and application predicates are useful when the condition itself is the goal. Locators are the recommended higher-level choice when the goal is to interact with an element. Navigation is a separate condition from a selector update: client-rendered content can change without a document navigation.
2. Set up a runnable Puppeteer example
Install Puppeteer in a Node.js project with npm install puppeteer. Save this as wait-condition.js and run node wait-condition.js. The script waits for an application result, reports the title, and closes the browser even if a wait fails.
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
page.setDefaultTimeout(10_000);
await page.goto('https://example.com');
// Replace this with a condition meaningful to your target page.
await page.waitForSelector('h1', { visible: true, timeout: 10_000 });
const heading = await page.locator('h1').map(el => el.textContent).wait();
console.log(heading);
} finally {
await browser.close();
}
})().catch(error => {
console.error(error);
process.exitCode = 1;
});
The site and selector are illustrative. For your page, choose a stable selector or predicate tied to the result you need, not merely an element that happens to render early.
3. Wait for an element to appear, show, or disappear
Wait for DOM presence
const result = await page.waitForSelector('.result', { timeout: 10_000 });
// result is an ElementHandle when found.
Without visible or hidden, the wait checks whether a matching element exists in the DOM. It resolves immediately if the selector already matches. A missing selector throws after the configured timeout.
Wait for visibility
await page.waitForSelector('.result', { visible: true, timeout: 10_000 });
The documented visibility check requires the element to be in the DOM and not have display: none or visibility: hidden. It does not prove the element is enabled, unobstructed, stable, or ready for every interaction. If the next step is clicking or typing, prefer a locator action.
Wait for disappearance or hidden state
await page.waitForSelector('.spinner', { hidden: true, timeout: 10_000 });
This resolves when the selector is absent or hidden. If it is already missing, the hidden wait resolves to null. That makes it useful for a loading indicator, but pair it with a positive completion condition when the page could fail silently—for example, wait for the spinner to disappear and then wait for the result selector.
4. Wait for a custom application condition
Use page.waitForFunction() when the state you need is not adequately expressed by selector presence or visibility. Its callback runs repeatedly in the browser page context until it returns a truthy value.
await page.waitForFunction(
() => document.querySelectorAll('.row').length >= 3,
{ timeout: 10_000 },
);
Pass values from Node.js as additional arguments. A callback running in the page cannot close over ordinary Node variables:
const selector = '.foo';
await page.waitForFunction(
selector => Boolean(document.querySelector(selector)),
{ timeout: 10_000 },
selector,
);
Keep the predicate focused on the state needed to proceed. For example, check a rendered status value or expected row count rather than waiting for an unrelated network request. When an observable condition exists, a fixed sleep can finish too early on a slow page or waste time on a fast one.
5. Use a locator when the goal is interaction
Puppeteer recommends locators for selecting and interacting with elements. A locator action waits for the element and checks action preconditions, which avoids treating DOM presence as proof that a click can succeed.
await page.locator('button.submit').click();
You can also wait for a locator state directly:
await page.locator('.result').wait();
Use waitForSelector() when you need an element handle or specifically need to observe DOM/visibility state. Use a locator action when the next operation is the point of waiting. A visibility wait alone does not establish that the target is enabled or unobstructed.
6. Wait for navigation after a click
Start the navigation wait before triggering the click. If you await the click first and only then call waitForNavigation(), navigation may already have started and the wait can miss it.
const [response] = await Promise.all([
page.waitForNavigation({ waitUntil: 'load', timeout: 30_000 }),
page.locator('a.my-link').click(),
]);
console.log('Navigation finished', response?.url());
The same ordering works with page.click():
await Promise.all([
page.waitForNavigation(),
page.click('a.my-link'),
]);
History API URL changes count as navigation according to the navigation wait reference. But many single-page applications update content without a document navigation. In that case, wait for the resulting selector or application predicate instead of waiting for navigation.
Choose a navigation lifecycle condition
The navigation wait defaults to waitUntil: 'load' and a 30-second timeout in the referenced API documentation. Choose a lifecycle condition that represents the point your task needs; navigation and application readiness are not interchangeable. Confirm the accepted options against the API reference for your installed version.
7. Configure timeouts and cancellation
| Configuration | Example | When to use |
|---|---|---|
| Per selector wait | { timeout: 10_000 } |
Give one condition a task-specific deadline. |
| Page default timeout | page.setDefaultTimeout(10_000) |
Set a consistent default for applicable page operations. |
| Disable selector wait timeout | { timeout: 0 } |
Only when an external cancellation or overall deadline reliably bounds the operation. |
| Cancel selector wait | { signal: abortController.signal } |
Stop waiting when the enclosing task is cancelled. |
The cited selector and navigation references document a default of 30,000 milliseconds. Do not assume every wait method accepts identical options: check that method’s reference. A timeout of zero can leave a job stuck indefinitely if the condition never occurs; cancellation and an outer task deadline are safer for long-running workers.
const controller = new AbortController();
const wait = page.waitForSelector('.result', {
timeout: 15_000,
signal: controller.signal,
});
// In an enclosing cancellation path, call:
// controller.abort();
await wait;
8. Handle frames, navigation, and detached elements
If the target is inside an iframe, wait in the relevant frame context rather than querying the main page for a selector that is not part of its document:
const frame = page.frames().find(candidate => candidate.url().includes('/embedded/'));
if (!frame) throw new Error('Embedded frame was not found');
await frame.waitForSelector('.result', { visible: true, timeout: 10_000 });
Frame.waitForSelector() works across navigations. By contrast, ElementHandle.waitForSelector() applies within the current element and does not work across navigation or if that element detaches. If navigation can replace the document, use the page or frame method rather than anchoring the wait to an element handle that may become stale. The retrieved Frame reference surfaced version 25.10.0 while the main Page references surfaced 25.12.0, so verify behavior against the installed version.
9. Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
TimeoutError from waitForSelector |
The selector never matched, the page did not reach the expected state, or the timeout was too short. | Confirm the selector in the correct page/frame, inspect the actual page state, and use a timeout appropriate to the task. Wait for the real completion signal. |
| The wait resolves, but clicking fails | Default selector waiting only proves DOM presence; even visibility does not guarantee enabled or unobstructed state. | Use page.locator(selector).click() so the interaction waits for its action preconditions. |
waitForNavigation hangs after a click |
The click may trigger only a client-side update, or the wait was set up after navigation began. | Register it before the click with Promise.all when navigation is expected; otherwise wait for a resulting selector or predicate. |
| The custom predicate never becomes truthy | It refers to a Node variable that is unavailable in page context, checks the wrong state, or targets a different frame. | Pass Node values as arguments, verify the condition in the page, and run the predicate in the relevant frame. |
| The wait succeeds on the wrong element state | A broad selector matched an early placeholder or hidden duplicate. | Narrow the selector or use visible: true, a locator action, or an app-specific predicate. |
| Wait fails after a navigation or detach | An ElementHandle points to a document or element that no longer exists. |
Use page.waitForSelector() or frame.waitForSelector() for waits that must survive navigation. |
| Wait lasts much longer than expected | A default timeout is in effect, or a timeout was disabled with zero. | Set an explicit per-wait timeout or page default, and use cancellation for abandoned work. |
10. Performance, reliability, and cost
Condition-based waits let the task proceed as soon as the observed condition is true. A fixed delay adds latency to fast pages and can still be too short for slow ones. Keep predicates focused and avoid waiting on a broader condition than the task requires. This is practical guidance inferred from how condition-based waits resolve, not a benchmark claim.
Reliability depends on choosing a stable signal. Prefer a result element, state value, or action precondition tied to the outcome over a generic lifecycle event when the application continues rendering after load. Set bounded timeouts, handle failures at the job boundary, and close the browser in a finally block. Puppeteer documentation provides API behavior, not a universal runtime or cost benchmark; actual runtime and infrastructure cost depend on the browser workload and environment.
11. Or skip the browser setup
If the goal is to capture a page rather than interact with it, ScreenshotNeo is a website screenshot API and MCP server. One GET request returns a PNG, JPEG, WebP, or PDF. The API offers wait options including a selector, delay, or network idle, along with custom CSS and JavaScript when needed. See the ScreenshotNeo 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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));
- Cookie and consent banners, newsletter popups, and chat widgets are removed before the shot; each cleanup step can be turned off.
- Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing; response headers say which page verdict and billing result applied.
- An MCP server lets Claude, Cursor, or another MCP client use
take_screenshot,get_page_info, andcapture_pdf. - 1,000 screenshots per month are free with no card; paid plans start at $5 for 3,000 screenshots. Every feature is on every plan.
Create a free ScreenshotNeo account for 1,000 screenshots a month with no card.
12. FAQ
Does waitForSelector() wait for an element that is already on the page?
Yes. If the selector matches when the call starts, the wait resolves immediately.
Does visible: true mean the element can be clicked?
No. It checks documented visibility conditions, not every interaction precondition. Use a locator action when you need to click.
Should I wait for networkidle for every page?
No single wait condition suits every page. Choose the state your task needs; a selector, predicate, locator action, or navigation wait may be the better signal.
Can I use waitForSelector() for content inside an iframe?
Use the frame containing the content, then wait in that frame’s context.


