ScreenshotNeo

BlogHow-to

How to Wait for a JavaScript Condition in Puppeteer

Use Puppeteer’s `page.waitForFunction()` for custom page conditions. Learn when to use selector waits or locators, pass Node.js values, set timeouts, and troubleshoot failures.

By the ScreenshotNeo team4 October 20266 min read

Use page.waitForFunction() when you need Puppeteer to wait until an arbitrary JavaScript condition in the page becomes truthy. Use page.waitForSelector() when the requirement is simply that an element appears, becomes visible, or becomes hidden. Use a locator when the condition is a precondition for interacting with an element.

The examples below use Puppeteer’s documented API. Check the Puppeteer version installed in your project if its behavior or types differ from the current documentation.

1. Wait for an arbitrary JavaScript condition

page.waitForFunction(fn, options, ...args) evaluates the supplied function in the browser page context until its result is truthy. For example, wait for an application status element to report that the results are ready:

await page.waitForFunction(() => {
  const status = document.querySelector('[data-status]');
  return status?.textContent === 'Ready';
});

The callback can inspect the page DOM and globals. It does not close over variables in your Node.js process. Pass Node-side values explicitly after the options object:

const selector = '.result';

await page.waitForFunction(
  selector => Boolean(document.querySelector(selector)),
  {},
  selector,
);

The predicate may be asynchronous. Puppeteer waits for its evaluation to produce a truthy result. Keep the predicate safe to evaluate repeatedly: use it to read state, not to perform an action that should happen only once.

2. Choose the wait that matches the condition

What must happen Use Behavior
A page value or custom predicate becomes true page.waitForFunction() Evaluates a browser-side function until its result is truthy.
A selector exists in the DOM page.waitForSelector(selector) Resolves when a match exists, including if it was already present.
A selector is visible page.waitForSelector(selector, { visible: true }) Waits for a matching element to be present and visible.
A selector is absent or hidden page.waitForSelector(selector, { hidden: true }) Waits until it is hidden or absent; it can resolve to null if absent.
An element should be ready for an interaction page.locator(...) Locators wait for relevant element states and are Puppeteer’s recommended interface for selecting and interacting.

waitForSelector() checks DOM presence by default, not visibility. Its default timeout is 30 seconds. For a function-based condition tied to an element workflow, a locator can also wait for a value:

const paragraphs = await page
  .locator(() => {
    const items = document.querySelectorAll('p');
    if (items.length >= 3) {
      return [...items].map(item => item.textContent);
    }
  })
  .wait();

Choose waitForFunction() for a page-level predicate or value. Choose a selector wait for selector state. Choose a locator when the next step is an element action or the condition naturally describes the element. The official references are the waitForFunction API, waitForSelector API, and page interaction guide.

3. Complete runnable Node.js example

This CommonJS script launches Chromium, opens a page, waits for a page-side status condition, reads the resulting text, and closes the browser. Install Puppeteer in your project first with npm install puppeteer.

const puppeteer = require('puppeteer');

async function main() {
  const browser = await puppeteer.launch({ headless: true });
  try {
    const page = await browser.newPage();
    await page.goto('https://example.com');

    await page.waitForFunction(
      () => document.querySelector('h1')?.textContent?.trim().length > 0,
      { timeout: 10_000 },
    );

    const heading = await page.$eval('h1', element => element.textContent.trim());
    console.log(heading);
  } finally {
    await browser.close();
  }
}

main().catch(error => {
  console.error(error);
  process.exitCode = 1;
});

The condition here waits for a non-empty heading; for a real application, replace it with the specific page state that means your work can continue. An example of waiting for a value supplied by Node is:

const expectedStatus = 'Ready';

await page.waitForFunction(
  expected => document.querySelector('[data-status]')?.textContent?.trim() === expected,
  { timeout: 15_000 },
  expectedStatus,
);

4. Set a timeout or cancel the wait

Wait operations have a 30,000 ms default timeout. Set a per-wait timeout when an operation needs a different limit:

await page.waitForFunction(
  () => window.appState?.complete === true,
  { timeout: 20_000 },
);

You can also set a page-wide default timeout with Page.setDefaultTimeout():

page.setDefaultTimeout(20_000);
await page.waitForFunction(() => window.appState?.complete === true);

timeout: 0 disables the timeout. Use it only when an unbounded wait is intentional: if the state can never occur, the script can remain stuck. Wait options also support an AbortSignal for cancellation:

const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 5_000);

try {
  await page.waitForFunction(
    () => window.appState?.complete === true,
    { signal: controller.signal, timeout: 0 },
  );
} finally {
  clearTimeout(timer);
}

See the official wait options reference for the current option type and cancellation details.

5. Common errors and troubleshooting

Symptom Likely cause Fix
The wait times out The predicate never becomes truthy, observes the wrong state, or has too little time for the page operation. Inspect the target page and predicate, verify the relevant frame, and choose a timeout appropriate to the operation.
A Node.js variable is undefined in the callback The callback runs in the browser page context and does not capture Node scope. Pass the value as an argument after the options object.
The selector wait resolves but the element cannot be used as expected Default selector waiting checks presence, not visibility or interaction readiness. Request { visible: true } when visibility is required, or use a locator for an interaction.
A hidden selector wait returns null The selector is absent, which satisfies the hidden condition. Handle null as the expected absent state, or use a different condition if the element must first exist.
The script hangs indefinitely The timeout was disabled with timeout: 0, and the condition cannot occur. Restore a finite timeout or provide an AbortSignal that cancels the wait.
The condition runs an action more than once A wait predicate is reevaluated while waiting. Make the predicate a read-only state check; perform one-time actions after the wait resolves.

When debugging a timeout, first confirm that the callback would return a truthy value if evaluated at the moment you expect. Then check that it reads the right selector or page state, that it runs in the frame containing that state, and that the timeout matches the actual operation.

6. Performance, reliability, and cost

A condition wait expresses the state your script needs and can finish as soon as that state is true. A fixed sleep waits for elapsed time regardless of whether the page is ready sooner, and may still be too short when the page is slower. Use a sleep only when elapsed time itself is the requirement.

Keep predicates small and observational. Avoid expensive DOM scans or side effects in a function that Puppeteer reevaluates. Set a bounded timeout and handle timeout or cancellation in the surrounding workflow so one page cannot hold up the rest of a job indefinitely. Puppeteer’s documentation specifies wait behavior and defaults, but does not publish a topic-specific performance benchmark; actual timing depends on the page and environment.

A local Puppeteer workflow requires your process to run and manage the browser. For repeated captures, also account for browser launch and page work in your own infrastructure and operational costs. There is no Puppeteer wait fee stated in the cited API documentation.

7. Or skip the browser setup

If your goal is a website screenshot rather than custom browser automation, ScreenshotNeo is a screenshot API and MCP server for developers. One GET request returns a PNG, JPEG, WebP, or PDF. The API documentation covers its options.

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}`);
  • Cookie banners are accepted and removed before the shot; newsletter popups and chat widgets are also removed. Each cleanup step can be turned off.
  • Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed. Response headers report the page verdict and billing status.
  • An MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf.
  • The free plan includes 1,000 screenshots a month with no card. Paid plans start at $5 for 3,000 screenshots.

Sign up for ScreenshotNeo and get 1,000 free screenshots a month with no card.

8. Frequently asked questions

Can the function wait for an asynchronous condition?

Yes. The page function may be asynchronous; the wait resolves when its evaluation produces a truthy result.

Does waitForSelector mean an element is visible?

No. By default it waits for DOM presence. Request visible: true when visibility is part of the requirement.

Should I use waitForFunction for every page wait?

No. Use the API that describes the state you need: a selector wait for selector state, a locator for element interaction workflows, and waitForFunction() for a general page predicate.