How to Wait for a Function to Return True in Puppeteer
Use page.waitForFunction to wait for a page-side condition. Learn how to pass arguments, choose polling, handle timeouts, and troubleshoot common issues.
Use and await Puppeteer’s page.waitForFunction() with a predicate that becomes truthy in the page context:
await page.waitForFunction(() => window.appReady === true);
Puppeteer reevaluates the predicate until it returns a truthy value, then resolves with a handle to that value. await ensures the next Node.js statement runs only after the condition succeeds. The predicate runs in the browser page, so Node.js variables are not available unless you pass them as arguments. See the Puppeteer API reference.
1. Basic usage
For a page-side flag set by your application, use a predicate with no arguments:
const result = await page.waitForFunction(() => window.appReady === true);
console.log('Ready; predicate result:', await result.jsonValue());
The wait accepts any truthy result. If you specifically need the Boolean true, compare the condition explicitly with === true. The returned value is a JSHandle; you can inspect it with jsonValue() or dispose of it when you no longer need it.
A complete minimal script using an existing Puppeteer installation and a page URL:
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
await page.goto('https://example.com');
await page.waitForFunction(() => document.readyState === 'complete');
console.log('Document finished loading');
} finally {
await browser.close();
}
})();
document.readyState is an example condition, not a guarantee that every application-specific request or widget has finished. Wait for the state your task actually depends on.
2. Pass arguments to the predicate
The argument order is: predicate, options object, then zero or more predicate arguments. The options object can be empty when you only need to pass arguments.
const selector = '.foo';
await page.waitForFunction(
selector => Boolean(document.querySelector(selector)),
{},
selector,
);
This avoids relying on a Node.js variable inside browser code. Multiple arguments work the same way:
const minimum = 3;
const selector = 'p';
await page.waitForFunction(
(selector, minimum) => document.querySelectorAll(selector).length >= minimum,
{ timeout: 10_000 },
selector,
minimum,
);
Use a serializable value such as a string or number for arguments. The predicate executes in the page context, where browser objects like document and window exist; Node.js objects and closures do not cross that boundary.
3. Set a timeout, polling mode, or cancellation signal
waitForFunction uses a documented default timeout of 30,000 milliseconds. Set timeout to a different number of milliseconds, or use timeout: 0 to disable the timeout. Disabling it can leave a script waiting indefinitely if the condition never becomes true. You can also change the page’s default timeout with page.setDefaultTimeout(). Options are documented in the WaitForFunctionOptions reference.
await page.waitForFunction(
() => window.appReady === true,
{ timeout: 10_000, polling: 100 },
);
Polling determines when Puppeteer reevaluates the predicate:
| Option | When it fits |
|---|---|
'raf' (default) |
Checks through animation frames. Useful when a visual or style change can make the predicate true. |
'mutation' |
Checks when the DOM changes. Fits conditions driven by added, removed, or changed DOM nodes. |
| A number, in milliseconds | Checks on a fixed interval. Useful for a condition that changes outside DOM mutations and needs a defined cadence. |
For example, use DOM-mutation polling when waiting for a node to appear:
await page.waitForFunction(
selector => document.querySelector(selector) !== null,
{ polling: 'mutation', timeout: 8_000 },
'#results',
);
Polling choice should follow what can change the condition. A DOM mutation wait will not help if the predicate depends only on a value that changes without a DOM mutation. An AbortSignal can cancel the wait when the caller needs to stop waiting:
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 5_000);
try {
await page.waitForFunction(
() => window.appReady === true,
{ signal: controller.signal, timeout: 0 },
);
} finally {
clearTimeout(timer);
}
Use either a finite timeout or explicit cancellation according to the task; avoid an unbounded wait without a cancellation path.
4. Choose between waitForFunction and a locator
Use a locator when the goal is to find or interact with an element and its built-in readiness checks describe what you need. Puppeteer’s guide recommends locators for interactions; locators wait for element state automatically. Use waitForFunction for an arbitrary page-side condition that does not map cleanly to element selection or readiness. See the page interactions guide.
| Need | Good starting point |
|---|---|
| Click or type into a matching element | Locator interaction |
| Wait for an application flag, count, or combined browser-side condition | page.waitForFunction() |
| Wait for an element’s existence or state | Locator when its readiness conditions fit; otherwise a predicate |
5. Common problems and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| The wait times out | The predicate never becomes truthy, the page navigated, or the predicate checks the wrong state. | Log or inspect the relevant page state, confirm the condition is reachable, and set a timeout appropriate to the operation. |
| “Variable is not defined” in the predicate | The predicate runs in the browser and cannot close over Node.js variables. | Pass values after the options object, as in (selector) => ... followed by {}, selector. |
| The wait misses a state change | The selected polling mode does not observe the change source. | Use 'mutation' for DOM changes, 'raf' for frame-visible changes, or a numeric interval for other state changes. |
| The predicate succeeds too early | It checks a broad signal, such as document load, rather than the specific content or state needed. | Check a specific selector, expected count, or app-ready flag. If necessary, combine conditions in one predicate. |
| The predicate is expensive | It performs large DOM scans or repeated work on every poll. | Keep it small and focused; precompute stable inputs and avoid broad queries where a targeted check works. |
| An asynchronous predicate keeps doing work | The function starts an async operation each time it is reevaluated. | Ensure repeated calls are safe and inexpensive. If practical, wait for the operation once outside the repeated predicate, then check its resulting page state. |
6. Performance, reliability, and cost
The predicate is evaluated repeatedly until it succeeds, times out, or is aborted. Keep it deterministic, quick, and free of side effects. Choose a polling mode that matches how the state changes; unnecessarily frequent numeric polling can cause extra browser work, while a slow interval can delay detection. There is no universal polling interval: select one based on the page and the tolerance for detection delay.
For reliability, wait on the condition required by the next action instead of assuming that navigation completion means the application is ready. Bound waits with a timeout, handle timeouts at the call site, and close the browser in a finally block so failures do not leak browser processes. Puppeteer itself is software; this API does not add a per-wait charge.
7. Or skip the browser setup
If your end goal is a screenshot rather than browser interaction, ScreenshotNeo offers a single GET request. The response can be PNG, JPEG, WebP, or PDF; 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 Bun.write('shot.webp', res);
In Node.js environments without Bun, save the response body with your preferred file-writing API, such as fs/promises. Replace YOUR_API_KEY with your key and use the URL you want to capture.
ScreenshotNeo accepts cookie and consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers report the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. See ScreenshotNeo or sign up free for 1,000 screenshots a month, no card required.
8. FAQ
Does waitForFunction require the predicate to return exactly true?
No. It resolves for any truthy value. Compare with === true if you specifically require the Boolean value.
Can the predicate be async?
Yes. Puppeteer’s API examples include asynchronous page-context predicates. Make sure repeated evaluation is safe and does not start unnecessary work each time.
How do I pass a selector?
Put an options object second, then the selector: await page.waitForFunction(sel => document.querySelector(sel), {}, '.item').
What is the default timeout?
The documented default is 30 seconds. Set timeout in milliseconds, configure the page default, use an AbortSignal, or set timeout to zero when you provide another cancellation strategy.
Should I use waitForFunction for every element wait?
No. Prefer a locator for selection and interaction when its automatic readiness checks cover the requirement; reserve a function wait for custom page-side logic.


