Recommended Free Tools
The reliable fix is to await page.waitForSelector() inside a sequential for...of loop, immediately before the action or extraction that needs the element. Also verify that the selector represents the next state you need. Puppeteer returns immediately when a matching element is already in the DOM, so waiting for the same persistent selector does not prove that a new result loaded.
The correct sequential loop
Use for...of and await when each item depends on the previous navigation, click, or extraction:
for (const item of items) {
await page.waitForSelector(item.selector, {
visible: true,
timeout: 10_000,
});
await processCurrentItem(page, item);
}
The loop does not start the next iteration until both the selector wait and processCurrentItem() finish. This is different from items.forEach(async item => { ... }): forEach does not await the promises returned by its callback, so the surrounding function can continue or finish while callbacks are still running.
When parallel work is intentional
If iterations are independent, start them deliberately and wait for all of them:
#1 Best Overall
await Promise.all(items.map(async item => {
await page.waitForSelector(item.selector, { visible: true });
return processCurrentItem(page, item);
}));
Do not use this pattern with one shared page when operations can navigate or mutate the same document. Separate pages or browser contexts are usually required for genuinely concurrent browser work.
Why a repeated wait can appear to do nothing
The official Page.waitForSelector() documentation states that the promise resolves immediately if the selector already exists. Therefore, this code may not wait for a fresh result:
for (const query of queries) {
await page.click('#next');
await page.waitForSelector('.results'); // may already be present
console.log(await page.$eval('.results', el => el.textContent));
}
If .results is reused for every page of results, it remains present while its text changes. Wait for an observable state transition instead: a result ID, changed text, a newly inserted item, or a page-specific selector.
Wait for a changed value in a single-page app
const previousId = await page.$eval('.result', el => el.getAttribute('data-id'));
await page.click('#next');
await page.waitForFunction(
oldId => document.querySelector('.result')?.getAttribute('data-id') !== oldId,
{ timeout: 10_000 },
previousId
);
const current = await page.$eval('.result', el => el.textContent);
console.log(current);
The exact condition must match the application’s DOM. A changed data-id, heading, URL, count, or loading marker is stronger evidence than the continued existence of a container.
Free tools Windows power users keep installed
One-click scans. No signup required.
Rank #2
What waitForSelector actually waits for
The current official API pages display Puppeteer 25.12.0. waitForSelector resolves with an ElementHandle when the selector appears and throws if it does not appear before the configured timeout. A hidden wait can resolve to null when the selector is absent. See the WaitForSelectorOptions reference for the option contract.
| Option or behavior | Meaning | Typical use |
|---|---|---|
| Default | Wait for DOM presence; not necessarily visibility | Elements that may be off-screen or visually hidden |
visible: true |
Require the element to be present and visible | Before clicking or reading user-visible content |
hidden: true |
Wait until the element is hidden or absent | Waiting for a spinner or overlay to finish |
timeout |
Per-call limit; documented default is 30,000 ms | Use a deliberate limit for each iteration |
timeout: 0 |
Disable the timeout | Only when an indefinite wait is explicitly intended |
signal |
Abort the wait with an AbortSignal |
Cancel work when a job or request is abandoned |
You can also set a global default with page.setDefaultTimeout(), but a per-iteration timeout makes failures easier to diagnose. Disabling timeouts can leave a worker stuck forever when a selector is misspelled or a page fails.
A complete page-by-page example
When each URL navigates to a new document, a stable content marker is usually sufficient. Dispose of the returned handle after extracting its data:
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
const page = await browser.newPage();
try {
for (const url of urls) {
await page.goto(url, { waitUntil: 'domcontentloaded' });
const article = await page.waitForSelector('main article', {
visible: true,
timeout: 10_000,
});
try {
console.log(await article.evaluate(el => el.textContent?.trim()));
} finally {
await article.dispose();
}
}
} finally {
await browser.close();
}
This works only when main article is a reliable marker for every URL. If a site renders an empty article shell first, add a stronger condition, such as a title or required child element, rather than increasing the timeout indefinitely.
Windows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallCrashes, 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 minuteUse the right document and the right API
Selectors inside iframes
A selector in an iframe is not in the main page document. Obtain the frame and wait there. The official Frame.waitForSelector() documentation covers frame-scoped waits and navigation:
const frame = page.frames().find(f => f.url().includes('/checkout'));
if (!frame) throw new Error('Checkout frame not found');
await frame.waitForSelector('input[name="cardnumber"]', {
visible: true,
timeout: 10_000,
});
Prefer locators for actions
Puppeteer’s page-interactions guide recommends locators for selecting and interacting. A locator performs action precondition checks and can retry an action when the page changes. waitForSelector is a lower-level availability check; it does not automatically retry your later click, and an ElementHandle can become stale after re-rendering.
| Need | Better choice | Reason |
|---|---|---|
| Confirm that a node exists | waitForSelector |
Direct, explicit DOM wait |
| Click, type, or select reliably | Locator | Action preconditions and retries are built in |
| Work in an iframe | Frame locator or frame method | The element belongs to the frame’s document |
| Wait for a value to change | waitForFunction or a locator condition |
Presence alone does not represent progress |
Troubleshooting loop failures
TimeoutError on one iteration
- Check selector spelling, escaping, and whether the page reached the expected URL.
- Log the loop item and current URL before waiting.
- Confirm the element is not inside an iframe or shadow-root boundary.
- Capture HTML or a screenshot at failure time to distinguish a page-state problem from a selector problem.
- Handle expected misses explicitly instead of setting
timeout: 0.
try {
await page.waitForSelector(item.selector, { visible: true, timeout: 10_000 });
} catch (error) {
console.error({ item, url: page.url(), error: String(error) });
throw error;
}
The wait resolves but data is stale
The selector probably persists across iterations. Record a previous value and wait for it to differ, or wait for a per-item selector such as [data-id="${id}"]. Also wait for a loading indicator to disappear when the application exposes one.
The click fails after the wait
The node may have been replaced between the wait and the click, covered by an overlay, or technically present but not actionable. Use a locator for the action, wait for an overlay to become hidden, or reacquire the element immediately before interacting.
Do these 3 things before closing this tab:
1Clear out junk files and repair common Windows errors2Scan for outdated or missing drivers - takes under a minute3Repair Windows errors before they cause bigger problemsRank #4
Iterations overlap unexpectedly
Look for forEach(async ...), an unawaited helper, or a promise stored without being awaited. Use sequential for...of for shared-page workflows and return or await every asynchronous operation.
The process hangs forever
Inspect global timeout settings and calls using timeout: 0. Restore a finite timeout and add an abort signal for cancellable jobs. A timeout is diagnostic evidence that the expected state was not observed; hiding it makes recovery harder.
Performance and reliability choices
- Use the narrowest selector that identifies the intended state; broad containers often exist before their contents are usable.
- Set timeouts from the page’s normal latency and your job’s SLA, not from guesswork. A very short timeout creates false failures; an unlimited one consumes workers.
- Wait for navigation and content in the order the site requires. For a click that triggers navigation, coordinate the click and navigation promises rather than waiting on an old element.
- Reuse a browser where appropriate, but isolate pages when tasks must be concurrent.
- Dispose of handles, close pages, and close the browser in
finallyblocks so a failed iteration does not leak resources.
Or skip the browser setup
If your goal is a clean screenshot rather than browser automation, ScreenshotNeo makes one API request and returns PNG, JPEG, WebP, or PDF. It accepts cookie and consent banners before capture, removes more than 60 known consent platforms plus newsletter popups and chat widgets, and charges only for clean shots. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed; response headers identify the page verdict and billing status.
For a URL such as Stripe, the one-call cURL request is:
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python:
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)
Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
See the ScreenshotNeo documentation for options such as full-page capture, CSS selectors, device presets, dark mode, custom JavaScript, waits, blocking, PDFs, caching, bulk jobs, and signed webhooks. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots. Sign up free.
Best Value
Frequently Asked Questions
What is Puppeteer’s documented default waitForSelector timeout?
The current 25.12.0 API documentation lists 30 seconds (30,000 milliseconds), configurable per call or with page.setDefaultTimeout().
Can waitForSelector wait for an element to disappear?
Yes. Pass hidden: true to wait until the selector is hidden or absent; a hidden wait may resolve to null when the selector is absent.
Should I use waitForSelector or a locator for a button click?
Use a locator when the goal is an action with automatic precondition checks and retries. Use waitForSelector when you specifically need a lower-level DOM-availability check.
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.

