Use an awaited for...of loop when each Puppeteer iteration depends on the previous one, and wait for a selector that identifies the state you actually need. A repeated waitForSelector() on an element that remains in the DOM returns immediately; it does not prove that new content has loaded.
Use an awaited loop for sequential work
Page.waitForSelector() returns a promise. If the next iteration must not begin until the current selector appears and its associated work finishes, use for...of and await both the wait and the dependent action:
for (const item of items) {
await page.waitForSelector(item.selector, {
visible: true,
timeout: 10_000,
});
await processCurrentItem(page, item);
}
This is sequential: Puppeteer waits for the selector, then completes processCurrentItem, before it advances to the next item. The example assumes each item.selector identifies the state needed for that particular iteration.
Why forEach(async …) can appear not to wait
items.forEach(async item => { ... }) does not make the outer flow wait for the promises returned by its callbacks. If later code assumes all callbacks have finished, it may run too early. Use the sequential for...of pattern above when order matters.
Quick wins for a faster PC:
Repair Windows errors before they cause bigger problemsFix Now →Scan for outdated or missing drivers - takes under a minuteDriver Scan →Clear out junk files and repair common Windows errorsFree Scan →#1 Best Overall
When concurrent work is intended
If iterations are independent and may safely run at the same time, collect their promises and await them deliberately with Promise.all:
await Promise.all(items.map(async item => {
await page.waitForSelector(item.selector, { visible: true });
await processIndependentItem(page, item);
}));
Concurrency is appropriate only when the tasks do not interfere with shared page state or depend on a specific order. Multiple simultaneous operations on one page can otherwise make it unclear which iteration caused a navigation or DOM change.
Check whether the selector is already present
Puppeteer’s official API documentation, which displays version 25.12.0, states that waitForSelector() resolves immediately if the selector already exists when the method is called. That behavior is often the real cause of a loop that seems to skip waiting. A persistent container, button, or loading-region element can match every iteration even though the result inside it has not changed.
Choose a selector that distinguishes the intended state. For example, if each result has its own item ID, wait for the selector for that ID rather than a shared result container. If a single-page app reuses one result element, capture its current identifier and wait for that identifier to change after the action that requests the next result.
Rank #2
const previousId = await page.locator('[data-result-id]')
.getAttribute('data-result-id');
await page.locator('button.next').click();
await page.waitForFunction(previous => {
const result = document.querySelector('[data-result-id]');
return result && result.getAttribute('data-result-id') !== previous;
}, {}, previousId);
This illustrates the condition, not a universal selector recipe. Confirm the attribute, action, and timing against the page’s actual DOM. Puppeteer documents waitForFunction(), but the correct change condition depends on the site.
Choose presence, visibility, or disappearance
By default, waitForSelector() waits for a match in the DOM; that does not mean it is visible to a user. Use visible: true when the next step requires a visible element. Use hidden: true when you need to wait until a match is hidden or absent.
// Wait for a matching element to be visible.
await page.waitForSelector('.results', { visible: true });
// Wait for a loading indicator to be hidden or absent.
await page.waitForSelector('.loading', { hidden: true });
A hidden wait can resolve to null when the selector is absent. Do not treat the returned value as a usable element unless you have checked which option you used and what the page state means.
Set a timeout and handle expected misses
The documented default timeout is 30 seconds. Set a per-call timeout when a particular step should fail sooner, or configure a page-wide default with page.setDefaultTimeout(). A value of timeout: 0 disables the timeout and can leave the script waiting indefinitely if the selector never appears.
PC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minutetry {
await page.waitForSelector('.results', {
visible: true,
timeout: 10_000,
});
await processResults(page);
} catch (error) {
console.error('Results did not become visible:', error);
}
Catch a timeout when a missing result is an expected per-item outcome and your program has a defined recovery path. Otherwise, allowing the error to surface can be preferable to silently treating an incomplete capture or extraction as success.
Use the correct page, frame, or interaction API
For content inside an iframe
A page-level wait checks the page context, not an arbitrary iframe’s document. Get the relevant Frame and call its waitForSelector() method for selectors inside that frame. The official Frame API documentation says the frame method waits in that frame and works across navigations.
const frame = page.frames().find(frame => frame.url().includes('/embedded-content'));
if (!frame) {
throw new Error('Target frame was not found');
}
await frame.waitForSelector('.embedded-result', {
visible: true,
timeout: 10_000,
});
Use a frame-identification condition that matches your page; the URL fragment above is only an example. If the frame is created asynchronously, locate it after it becomes available rather than assuming it exists at the start.
For clicking or typing, consider a locator
Puppeteer’s current page-interactions guide recommends locators for selecting and interacting with elements. A locator waits for action preconditions and retries actions when appropriate. That can be a better fit than manually waiting for a selector and then separately trying to click it.
Rank #4
await page.locator('button.submit').click();
Use waitForSelector() when the task is specifically to wait for DOM availability or when you need the returned element handle. Prefer a locator when the goal is an interaction and its automatic waiting behavior fits your needs.
Dispose of element handles when finished
A successful waitForSelector() returns an ElementHandle. If you retain that handle for extraction, dispose of it when the work is done so the remote object is released:
const result = await page.waitForSelector('main article', {
visible: true,
timeout: 10_000,
});
if (!result) {
throw new Error('Article was not found');
}
try {
console.log(await result.evaluate(element => element.textContent));
} finally {
await result.dispose();
}
Complete example: visit URLs one at a time
This example waits for a visible article on each URL, extracts its text, and disposes of the returned handle. It uses the Puppeteer package in an existing Node.js project; install and configure Puppeteer according to the environment where the browser will run.
const puppeteer = require('puppeteer');
async function main() {
const urls = [
'https://example.com/one',
'https://example.com/two',
];
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
for (const url of urls) {
await page.goto(url);
const article = await page.waitForSelector('main article', {
visible: true,
timeout: 10_000,
});
if (!article) {
throw new Error(`Article was not found at ${url}`);
}
try {
const text = await article.evaluate(element => element.textContent);
console.log(url, text);
} finally {
await article.dispose();
}
}
} finally {
await browser.close();
}
}
main().catch(error => {
console.error(error);
process.exitCode = 1;
});
Replace the example URLs and selector with the pages and content marker you need. This loop is suitable when each navigation should finish and each page’s marker should appear before extraction. If navigation succeeds but the same selector is not a reliable marker for the new page’s content, wait for a more specific state instead.
Best Value
Troubleshoot common loop failures
| Symptom | Likely cause | Fix |
|---|---|---|
| The loop continues without an apparent wait | The selector already matches an element, so Puppeteer resolves immediately. | Wait for a per-item selector or a changed value such as a result ID, not a persistent container. |
| Code after the loop runs too early | Async callbacks passed to forEach() are not awaited by the outer flow. |
Use for...of with await for ordered work, or await a deliberate Promise.all() for independent work. |
| The wait times out although the element appears in the browser | The selector may be misspelled, the page may be in a different state, or the element may live in an iframe. | Verify the selector and page state; if it is in a frame, call waitForSelector() on the relevant Frame. |
| The match exists but cannot be interacted with as expected | The default wait checks DOM presence, not visibility or all action preconditions. | Use visible: true where appropriate, or use a locator for the interaction. |
| The script hangs on a missing selector | The timeout may have been disabled with timeout: 0. |
Restore a finite timeout unless an indefinite wait is intentional; handle an expected timeout explicitly. |
| Long-running extraction accumulates handles | Returned ElementHandle objects are not being released. |
Call dispose() in a finally block after extracting what you need. |
Or skip the browser setup
If your goal is a screenshot rather than DOM interaction, ScreenshotNeo can return a page image or PDF with one GET request. Cookie banners are accepted and removed before capture, along with 60+ known consent platforms, newsletter popups, and chat widgets; each cleanup step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the page verdict and billing status in headers. ScreenshotNeo also provides an MCP server with take_screenshot, get_page_info, and capture_pdf tools for AI agents.
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. ScreenshotNeo is a website screenshot API and MCP server by Yorker Media; its options include full-page capture, CSS-selector element capture, viewport and device settings, PDF output, custom CSS and JavaScript, waits, request blocking, caching, signed links, async jobs, and bulk capture. Plans include 1,000 shots per month free with no card; paid plans start at $5 for 3,000 shots. Learn about ScreenshotNeo or sign up for 1,000 free screenshots a month with no card.
Official references
- Puppeteer Page.waitForSelector() method and WaitForSelectorOptions, official documentation displaying version 25.12.0.
- Puppeteer Page interactions, official guide displaying version 25.12.0.
- Puppeteer Frame.waitForSelector() method, official repository documentation.
Frequently Asked Questions
What does waitForSelector() return?
It resolves with an ElementHandle when a matching selector is found; with a hidden wait, it can resolve to null when the selector is absent.
Can waitForSelector() be cancelled?
The documented options include an AbortSignal, which can be used to cancel a wait.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

