Puppeteer WaitFor Options Explained
Learn when to use Puppeteer’s navigation, selector, and function waits, how their options differ, and how to avoid timing races and timeout errors.
Puppeteer waits are synchronization controls: choose the condition that means the next step can safely run. Use WaitForOptions for navigation waits, waitForSelector for DOM presence or visibility, and waitForFunction for an application-specific predicate. Navigation waits default to the load lifecycle event and 30,000 milliseconds; selector waits also default to 30,000 milliseconds. A timeout of 0 disables the timeout.
WaitForOptions does not control selector visibility. Its options are signal, timeout, and waitUntil. Selector waits have their own options: hidden, visible, signal, and timeout. Puppeteer WaitForOptions reference · WaitForSelectorOptions reference.
1. Choose the wait that matches readiness
| Need to wait for | Use | What success means |
|---|---|---|
| An element to exist | page.waitForSelector(selector) |
A matching DOM element exists. It may resolve immediately if the element is already present. |
| An element to be visible | page.waitForSelector(selector, { visible: true }) |
A matching element exists and is not hidden by display:none or visibility:hidden. |
| An element to be absent or hidden | page.waitForSelector(selector, { hidden: true }) |
No match exists, or the match is hidden by those CSS states. Absence counts as success. |
| A custom page condition | page.waitForFunction(fn, options, ...args) |
The supplied function evaluates to a truthy value in the page. |
| A navigation caused by an action | page.waitForNavigation(options) with the action |
The configured navigation lifecycle condition is met. Start the wait before triggering the action. |
Pick the narrowest condition that represents what the next step actually needs. A lifecycle event does not prove that a particular application widget is ready, and a visible element does not prove it is unobscured or on screen.
2. Understand WaitForOptions
The current Puppeteer API reference describes this interface for navigation-style waits:
| Option | Purpose | Default and behavior |
|---|---|---|
timeout |
Maximum wait in milliseconds | 30,000 ms. Use 0 to disable the timeout. Page defaults can be adjusted with setDefaultTimeout() or setDefaultNavigationTimeout(). |
signal |
Cancel the wait with an AbortSignal |
Optional. |
waitUntil |
Choose the lifecycle event or events that complete the wait | load. An array succeeds only after every listed event has fired. |
Supported lifecycle conditions include load, domcontentloaded, networkidle0, and networkidle2. Network idle conditions describe network activity thresholds; they are not application-specific guarantees. Select the event that matches the work that follows, rather than assuming one setting is right for every page.
Navigation example with a direct URL
const response = await page.goto('https://example.com', {
waitUntil: 'domcontentloaded',
timeout: 30_000,
});
console.log('HTTP status:', response?.status());
For a pinned older Puppeteer release, check that release’s API reference and installed types before relying on version-specific behavior.
3. Wait for selectors: presence, visibility, or disappearance
Page.waitForSelector() is usually the clearest choice when readiness is represented by a DOM element. Its options are not WaitForOptions:
| Selector option | Meaning |
|---|---|
visible: true |
Wait for a matching element that is not display:none or visibility:hidden. |
hidden: true |
Wait for no matching element or for a match in one of those hidden CSS states. It can resolve to null when there is no match. |
timeout |
Maximum wait in milliseconds; default is 30,000 and 0 disables the timeout. |
signal |
Optional cancellation signal. |
// Presence: resolves immediately if the element already exists.
const result = await page.waitForSelector('[data-testid="results"]', {
timeout: 10_000,
});
if (!result) throw new Error('Expected results element');
// Visibility: useful when the element is initially hidden.
await page.waitForSelector('.loading-indicator', { hidden: true });
await page.waitForSelector('[data-testid="results"]', { visible: true });
“Visible” here has a specific documented meaning; it does not certify that the element is in the viewport, unobscured, or ready for every interaction. If your next action has additional requirements, express and wait for those too.
4. Wait for an application predicate
Use waitForFunction when the readiness rule is not a simple selector or navigation event, such as a page-owned state value or a minimum number of rendered results. It repeatedly evaluates the supplied function in the page context until the result is truthy. The predicate defines what success proves.
await page.waitForFunction(
minimum => document.querySelectorAll('[data-testid="result"]').length >= minimum,
{ polling: 'mutation', timeout: 15_000 },
5,
);
console.log('At least five results are present.');
| Polling option | When it fits |
|---|---|
'raf' |
Recheck on animation frames; this is the documented default. |
'mutation' |
Recheck when DOM mutations occur, useful when the condition follows DOM changes. |
| A number in milliseconds | Recheck on a fixed interval when a timed poll suits the condition. |
Function waits accept a timeout and an optional abort signal as well. Keep predicates cheap: they may run repeatedly. Avoid waiting for an overly broad condition that can remain false because of unrelated page behavior.
5. Register navigation waits before the action
A click can navigate before a separately started navigation wait is registered. Start both operations together with Promise.all:
const [response] = await Promise.all([
page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: 30_000 }),
page.click('a.next'),
]);
console.log('Navigation response:', response?.status());
This pattern avoids the race between the click and the wait. Choose waitUntil based on the following task. For example, waiting for domcontentloaded can be sufficient when the next step only needs the initial document structure; a later selector or predicate may still be needed for asynchronously rendered content.
6. Set timeouts and cancel waits deliberately
Use per-call timeouts for exceptional operations and page-level defaults when a consistent policy suits the workflow. setDefaultTimeout() adjusts the general default; setDefaultNavigationTimeout() adjusts navigation defaults. The documented default is 30 seconds. Setting a timeout to 0 removes that limit, which can leave a stalled job waiting indefinitely unless another cancellation or external limit exists.
page.setDefaultTimeout(12_000);
page.setDefaultNavigationTimeout(25_000);
const controller = new AbortController();
const wait = page.waitForSelector('[data-testid="ready"]', {
timeout: 20_000,
signal: controller.signal,
});
// Cancel if the surrounding task is abandoned or superseded.
setTimeout(() => controller.abort(), 5_000);
try {
await wait;
} catch (error) {
console.error('Readiness wait ended:', error.message);
}
Cancellation is supported on the navigation, selector, and function wait options described in the current references. Handle cancellation separately from a readiness timeout if your application needs different recovery behavior.
7. Element handles and frame waits have different lifetimes
ElementHandle.waitForSelector() is tied to the current element and does not work across navigations or after that element is detached. A frame-level selector wait works across navigations. If navigation or DOM replacement is possible, prefer a page/frame-level wait unless the current element’s lifetime is exactly what you intend to monitor. See the ElementHandle method reference and Frame method reference.
8. Complete runnable Node.js example
This example launches Chromium through Puppeteer, navigates, waits for an application selector, and closes the browser even if a wait fails. Install Puppeteer in the project with npm install puppeteer, then save as wait.js and run node wait.js.
const puppeteer = require('puppeteer');
async function main() {
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
page.setDefaultTimeout(15_000);
page.setDefaultNavigationTimeout(30_000);
await page.goto('https://example.com', {
waitUntil: 'domcontentloaded',
});
await page.waitForSelector('h1', { visible: true });
const heading = await page.$eval('h1', el => el.textContent.trim());
console.log(heading);
} finally {
await browser.close();
}
}
main().catch(error => {
console.error(error);
process.exitCode = 1;
});
The example uses a selector as its readiness condition. For a real application, replace h1 with the element that signals the data or state your next operation requires.
9. Troubleshooting common wait failures
| Symptom | Likely cause | Fix |
|---|---|---|
TimeoutError waiting for a selector |
The selector never appears, differs from the page’s actual DOM, or the page is still rendering. | Inspect the rendered page and selector; wait for the correct state; increase the timeout only if the condition legitimately takes longer. |
| A selector wait resolves too early | The code waited for presence, but the next step requires visible or application-ready content. | Use visible:true or a focused waitForFunction predicate. |
hidden:true resolves immediately |
No matching element exists yet; absence already satisfies the option. | If you need to observe a loading element disappear, first ensure it exists or wait on the actual completion condition. |
| Navigation wait times out after a click | The click did not navigate, or the wait was registered after navigation began. | Use Promise.all with the navigation wait created before the click. If the action updates content without navigation, wait for a selector or predicate instead. |
| Navigation wait completes but content is missing | The lifecycle event occurred before client-side rendering or data loading finished. | After navigation, wait for the element or application predicate needed by the next step. |
| Wait never ends with timeout disabled | timeout: 0 disables the timeout; the condition may never become true. |
Use a bounded timeout or cancellation signal, and ensure cleanup runs after failures. |
| Element handle wait fails after route change | The handle is detached or belongs to the previous document. | Use a page/frame-level wait after navigation and reacquire the element. |
| Function wait burns time or CPU | The predicate is expensive or polls more often than needed. | Make the predicate cheap and select mutation, raf, or a numeric interval to match how the condition changes. |
10. Performance, reliability, and cost notes
- Reliability: State-based waits are usually easier to reason about than fixed sleeps because they resolve when the condition becomes true. They can still time out if the condition is wrong or the page never reaches it.
- Performance: A fixed delay always spends its full duration even when the page is ready sooner. Prefer selector or predicate waits; keep function predicates inexpensive and polling appropriate to the signal.
- Navigation: Lifecycle waits can be longer or shorter depending on page behavior. A page with continuing requests may make a network-idle condition unsuitable; use the condition that matches the actual next operation.
- Timeout policy: Per-call limits make slow steps explicit; page defaults reduce repetition. Avoid unlimited waits in unattended jobs without a separate cancellation mechanism.
- Cost: Puppeteer wait options do not specify a per-wait charge. Operational cost depends on your browser runtime and how long jobs occupy it; prompt, specific waits help avoid unnecessary idle time.
11. Or skip the browser setup
If the goal is a screenshot rather than browser automation, ScreenshotNeo provides a website screenshot API and MCP server. A single request captures a URL as an image or PDF:
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
See the ScreenshotNeo API documentation for request options. Python:
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
Node.js:
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 require('node:fs/promises').writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
ScreenshotNeo accepts cookie and consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before the shot; each step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, with response headers indicating page verdict and billing. Its MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots monthly with no card; paid plans start at $5 for 3,000. Sign up for 1,000 free screenshots a month, with no card required.
12. FAQ
What is the default Puppeteer timeout?
The documented default for these waits is 30,000 milliseconds. Page-level timeout settings and per-call options can change it.
Does visible:true mean the element is on screen?
No. The documented condition excludes display:none and visibility:hidden; it does not guarantee viewport position or lack of overlap.
Does waitForFunction guarantee the page is ready?
It guarantees only that the function you supplied evaluated truthy. Define the predicate around the exact state the next operation needs.
Can a selector wait succeed if the element already exists?
Yes. A presence wait may resolve immediately when the selector already matches.
When should I use waitUntil versus visible?
Use waitUntil to select navigation lifecycle events. Use visible on selector waits to require the documented visible CSS state; they configure different waits.


