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.
Using an older Puppeteer version? The general-purpose
page.waitFor()was deprecated and removed. Puppeteer 22 also removedpage.waitForTimeout()andpage.waitForXPath(). The modern replacements are shown below.
Which wait should you use?
| You need to wait for… | Use… |
|---|---|
| An element to appear, disappear, or become visible | page.waitForSelector() |
| A custom condition inside the page | page.waitForFunction() |
| A click or form submission to navigate | page.waitForNavigation() |
| A particular outgoing request | page.waitForRequest() |
| A particular server response | page.waitForResponse() |
| Network activity to become quiet | page.waitForNetworkIdle() |
| An iframe to appear | page.waitForFrame() |
| A click to open a file chooser | page.waitForFileChooser() |
| A fixed delay | setTimeout() from node:timers/promises |
| An element matched by XPath | page.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.
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());
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/orders") &&
request.method() === "POST",
),
page.click("button.place-order"),
]);
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/orders") &&
response.request().method() === "POST" &&
response.status() === 201,
{ timeout: 15_000 },
),
page.click("button.place-order"),
]);
const order = await response.json();
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 paymentFrame = await page.waitForFrame(
frame => frame.url().includes("/embedded-payment"),
{ timeout: 10_000 },
);
await paymentFrame.locator('input[name="card-number"]').fill("4242424242424242");
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.
Wait for a fixed timeout
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() was removed in Puppeteer 22. A fixed sleep should normally be the last choice because it is either longer than necessary or too short on a slow run.
Wait for XPath
The old page.waitForXPath() was also removed in Puppeteer 22. Pass Puppeteer’s XPath selector syntax to waitForSelector() instead:
const continueButton = await page.waitForSelector(
'::-p-xpath(//button[normalize-space()="Continue"])',
{ visible: true },
);
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:
- Timed out after waiting ms
- Waiting failed: ms exceeded
- No element found for selector
- Execution context was destroyed
- Frame detached
- Unknown value for
options.waitUntil
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.