(Created on )

Waiting in Puppeteer: A Practical Guide

Simple examples for waiting on selectors, functions, navigation, network activity, frames, file choosers, and more in Puppeteer.


Most flaky Puppeteer scripts wait for time to pass. Reliable scripts wait for the page state or browser event they actually need.

For example, wait for a result to become visible instead of sleeping for two seconds and hoping it is ready:

await page.waitForSelector(".search-result", { visible: true });

This guide shows the main waiting APIs, when to use each one, and the pattern that avoids race conditions.

A complete inline waiting example

Run npm install puppeteer in your project. Save this as waiting.mjs and run node waiting.mjs; the HTML is included:

import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({headless: true});
try {
  const page = await browser.newPage();
  await page.setContent(`<script>
    setTimeout(() => {
      const result = document.createElement('p');
      result.id = 'result'; result.textContent = 'Ready';
      document.body.append(result);
    }, 100);
  </script>`);
  const result = await page.waitForSelector('#result', {timeout: 5000});
  if (!result) throw new Error('Required result absent');
  try {console.log(await result.evaluate(element => element.textContent));}
  finally {await result.dispose();}
} finally {await browser.close();}

It prints Ready after the element appears. The focused patterns below assume a live page and the actual application’s controls, URLs and result conditions. Register waits before actions and do not blindly retry side effects.

Replacing old waiting methods? The general-purpose page.waitFor(), page.waitForTimeout() and page.waitForXPath() methods have been removed. Their replacements are shown below.

Which wait should you use?

You need to wait for…Use…
An element to appear, disappear, or become visiblepage.waitForSelector()
A custom condition inside the pagepage.waitForFunction()
A click or form submission to navigatepage.waitForNavigation()
A particular outgoing requestpage.waitForRequest()
A particular server responsepage.waitForResponse()
Network activity to become quietpage.waitForNetworkIdle()
An iframe to appearpage.waitForFrame()
A click to open a file chooserpage.waitForFileChooser()
A fixed delaysetTimeout() from node:timers/promises
An element matched by XPathpage.waitForSelector() with an XPath selector

For element actions such as clicking and filling inputs, first consider a locator. Locators automatically wait for an element to be present and ready for the action.

Wait for a selector

Use page.waitForSelector() when a specific DOM element is your readiness signal.

const results = await page.waitForSelector(".search-results", {
    visible: true,
    timeout: 10_000,
});

// Use the element, then release the handle when it is no longer needed.
if (!results) throw new Error("Required result absent");
await results.click();
await results.dispose();

Use visible: true when the element must be displayed, not merely present in the DOM. Use hidden: true to wait for a spinner, modal, or other element to disappear:

await page.waitForSelector(".loading-spinner", { hidden: true });

If your next step is a normal element action, a locator is usually simpler:

await page.locator('button[type="submit"]').click();

If a selector never matches, see No element found for selector, Failed to find element matching selector, and Waiting failed: ms exceeded.

Wait for a function

Use page.waitForFunction() for a condition that cannot be described by one selector. The function runs in the browser and is retried until it returns a truthy value.

const ready = await page.waitForFunction(
    minimum => document.querySelectorAll(".search-result").length >= minimum,
    { timeout: 10_000 },
    3,
);

await ready.dispose();

Arguments after the options object are passed safely from Node.js into the page function. Avoid closing over Node.js variables because the function runs in a different JavaScript context.

If you configure interval polling, it must be greater than zero; see Cannot poll with non-positive interval.

Wait for navigation

Use page.waitForNavigation() when an action indirectly loads a new document, reloads the current page, or changes the URL through the History API.

Start the wait before the action. Promise.all() makes that ordering explicit and prevents fast navigations from being missed:

const [response] = await Promise.all([
    page.waitForNavigation({
        waitUntil: "domcontentloaded",
        timeout: 30_000,
    }),
    page.click("a.checkout"),
]);

console.log(response?.status());

waitForNavigation() can return null for same-document/blank transitions. If your task requires a new HTTP document, reject null and non-OK responses; otherwise validate the expected URL/DOM transition explicitly. A later arbitrary response is not a replacement document response.

If the action replaces the document, query elements again afterward instead of keeping old element handles. See Execution context was destroyed, Unknown value for options.waitUntil, and the longer guide to handling navigation errors.

Wait for a request

Use page.waitForRequest() when you need to know that the page sent a particular request. As with navigation, register the wait before triggering the request.

const [request] = await Promise.all([
    page.waitForRequest(
        request =>
            request.url().endsWith("/api/save") &&
            request.method() === "POST",
    ),
    page.click("#ajax button"),
]);

console.log(request.postData());

This tells you that the request was sent. It does not tell you whether the server accepted it.

Wait for a response

Use page.waitForResponse() when the server response is the readiness signal. Match narrowly enough that an unrelated request cannot resolve the wait.

const [response] = await Promise.all([
    page.waitForResponse(
        response =>
            response.url().endsWith("/api/save") &&
            response.request().method() === "POST" &&
            response.status() === 200,
        { timeout: 15_000 },
    ),
    page.click("#ajax button"),
]);

if (!response.ok()) throw new Error(`Save HTTP ${response.status()}`);
await page.waitForFunction(() => document.querySelector("#status").textContent === "Saved");

Prefer this to network idle when one known API call determines whether the page is ready.

Wait for network idle

Use page.waitForNetworkIdle() when you need the whole page to stay quiet for a short period, for example before taking a screenshot of a page that loads several late resources.

await page.waitForNetworkIdle({
    idleTime: 500,
    timeout: 10_000,
});

Do not use network idle as a universal definition of “done.” Analytics, polling, streaming, and long-lived connections can keep a page busy even when the useful content is ready. A specific selector or response is usually more reliable.

Wait for a frame

Use page.waitForFrame() when an iframe is created asynchronously and you need to work inside it.

const resultFrame = await page.waitForFrame(
    frame => frame.name() === "result",
    { timeout: 10_000 },
);

await resultFrame.waitForSelector("#result");
console.log(await resultFrame.$eval("#result", element => element.textContent));

Always query inside the returned frame. A selector on page only searches the main frame. If the iframe is replaced or removed, find the new frame rather than reusing the old handle; see Frame detached.

Wait for a file chooser

Use page.waitForFileChooser() when a page opens a native file picker after a click. The listener must be active before the picker opens.

const [fileChooser] = await Promise.all([
    page.waitForFileChooser(),
    page.click("button.upload"),
]);

await fileChooser.accept(["/absolute/path/to/report.pdf"]);

For a plain <input type="file">, uploadFile() on its element handle may be more direct. waitForFileChooser() is useful when a separate button or script triggers the chooser. A chooser can be handled only once; see Cannot accept FileChooser which is already handled and Cannot cancel FileChooser which is already handled.

Why page.waitForTimeout is not a function

Fixed delays are occasionally useful for debugging, rate limiting, or waiting on something Puppeteer cannot observe. Use the Node.js promise-based timer in current Puppeteer code:

import { setTimeout as delay } from "node:timers/promises";

await delay(500);

The old page.waitForTimeout() method has been removed. A fixed sleep should normally be the last choice because it is either longer than necessary or too short on a slow run.

Replacing every removed call with a sleep preserves the old flakiness; migrate to the actual selector/function/event signal when one exists.

Choose waitUntil deliberately

For supported CDP navigation, choose load, domcontentloaded, networkidle0 or networkidle2, or a documented combination. networkidle2 is not declared deprecated merely because a keyword asks that question. These lifecycle signals do not prove application success; follow them with the relevant content check. Unsupported names cause waitUntil validation errors.

Wait for XPath

The old page.waitForXPath() method has also been removed. Pass Puppeteer’s XPath selector syntax to waitForSelector() instead:

const continueButton = await page.waitForSelector(
    '::-p-xpath(//button[normalize-space()="Continue"])',
    { visible: true },
);

if (!continueButton) throw new Error("Continue button absent");
await continueButton.click();
await continueButton.dispose();

Prefer a stable CSS or ARIA selector when one is available. XPath is useful when the relationship between elements matters and the page does not expose reliable attributes.

Replace the old page.waitFor() overloads

The legacy page.waitFor() changed behavior based on the type of its first argument. Migrate each form to the explicit API:

// Old: await page.waitFor(".result");
await page.waitForSelector(".result");

// Old: await page.waitFor(() => window.appReady);
await page.waitForFunction(() => window.appReady);

// Old: await page.waitFor(500);
await delay(500);

The explicit methods are easier to read and avoid the old ambiguity between CSS and XPath strings.

Timeouts and failures

Most Puppeteer waits time out rather than hanging forever. Set defaults once, then override them only where a particular operation genuinely needs more or less time:

page.setDefaultTimeout(10_000);
page.setDefaultNavigationTimeout(30_000);

Increasing a timeout does not fix a condition that can never become true. First check the selector, frame, URL predicate, response status, and whether the action that should trigger the event actually ran.

For diagnosis and recovery examples, see:

The central rule is simple: start event-based waits before the action that can trigger them, and wait for the narrowest observable condition that proves your next step is safe.

Keep Reading

Puppeteer Guides

All Guides →