A CSS selector in Puppeteer is just a JavaScript string. Store it in a variable and pass that variable directly to a selector-taking method such as page.$(), page.$eval(), or page.waitForSelector():
const selector = '.result';
const element = await page.$(selector);
Do not put the variable name in quotes. page.$(selector) uses the selector’s value; page.$('selector') searches for an element literally matching the word selector.
Pass the selector variable directly
Most Puppeteer methods that select elements expect a selector as their first argument. A function parameter works exactly like any other string value:
import puppeteer from 'puppeteer';
async function findResult(page, selector) {
return page.$(selector);
}
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://example.com');
const result = await findResult(page, '.result');
console.log(result ? 'Found the element' : 'No match');
await browser.close();
Here, selector is a parameter containing '.result'. Puppeteer receives that string at runtime and interprets it as a selector.
The Tool Desk
Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →#1 Best Overall
Do not quote the parameter name
async function getElement(page, selector) {
const correct = await page.$(selector); // Uses the parameter value
const incorrect = await page.$('selector'); // Searches for a literal selector named "selector"
return { correct, incorrect };
}
Use quotes only when you are writing a literal selector directly, such as page.$('.result').
Choose the Puppeteer method for the job
| Method | Call shape | Waits? | When there is no match | Result |
|---|---|---|---|---|
page.$ |
page.$(selector) |
No | Resolves to null |
First matching ElementHandle |
page.$eval |
page.$eval(selector, callback) |
No | Throws an error | Callback’s returned value |
page.waitForSelector |
page.waitForSelector(selector, options) |
Yes | Throws after the timeout | ElementHandle |
page.evaluate |
page.evaluate(callback, selector) |
No, unless your callback waits | Your callback decides | Serializable value returned by the page function |
The official Puppeteer API references document these signatures for the current documentation, including the Page.$eval and Page.evaluate pages identified as version 25.12.0. APIs can differ across historical releases, so check the version installed in your project.
Use a parameter with page.$()
Use page.$(selector) when you need a handle to one element and the element might be optional. It resolves to null when no element matches.
async function getOptionalCard(page, selector) {
const card = await page.$(selector);
if (!card) {
return null;
}
const text = await card.evaluate(node => node.textContent?.trim() ?? '');
await card.dispose();
return text;
}
const title = await getOptionalCard(page, '.card-title');
Dispose an ElementHandle when you no longer need it, especially in loops or long-running workers.
Use a parameter with page.$eval()
page.$eval() selects the first matching element, passes it as the callback’s first argument, and returns the callback result. Its argument order is selector, callback, then any additional callback arguments.
Rank #2
async function readText(page, selector) {
return page.$eval(selector, element => element.textContent?.trim() ?? '');
}
const text = await readText(page, '.result');
console.log(text);
If the selector matches nothing, $eval throws. Catch that error when a missing element is an expected condition:
async function readOptionalText(page, selector) {
try {
return await page.$eval(selector, element => element.textContent?.trim() ?? '');
} catch (error) {
if (error instanceof Error && /failed to find element/i.test(error.message)) {
return null;
}
throw error;
}
}
Forward extra arguments separately
Arguments after the callback are not additional selector arguments. They are values delivered to the callback:
async function readAttribute(page, selector, attributeName) {
return page.$eval(
selector,
(element, name) => element.getAttribute(name),
attributeName,
);
}
const href = await readAttribute(page, 'a.download', 'href');
Puppeteer supplies the matched element first; attributeName arrives as the callback’s second parameter.
Free tools Windows power users keep installed
One-click scans. No signup required.
Use page.evaluate() when the query belongs in page code
With page.evaluate(), the first argument is a function. Values after that function are serialized and passed into its parameters. The selector is therefore an evaluation argument, not a Puppeteer selector argument:
async function readTextInPage(page, selector) {
return page.evaluate(
sel => document.querySelector(sel)?.textContent?.trim() ?? null,
selector,
);
}
const value = await readTextInPage(page, '.result');
This approach is useful when you need several DOM operations in one page-context function. The selector is evaluated by document.querySelector, so an invalid CSS selector causes a browser-side exception.
Choosing between $eval and evaluate
- Use
$evalwhen Puppeteer should perform the selection and you need one matched element. - Use
evaluatewhen the entire query and transformation should run inside page JavaScript, or when you need a custom no-match result. - Use
$$eval(the same selector-first idea) when you need all matching elements and want to map them to serializable data.
Wait for a dynamic element before using the parameter
If the page renders the target later, call page.waitForSelector(selector, options). The documented default timeout is 30,000 milliseconds. You can require visibility, wait for hiding, set a different timeout, or provide an abort signal.
async function readLoadedResult(page, selector) {
await page.waitForSelector(selector, {
visible: true,
timeout: 10_000,
});
return page.$eval(selector, element => element.textContent?.trim() ?? '');
}
const result = await readLoadedResult(page, '[data-testid="result"]');
Waiting and extraction are separate operations: waitForSelector confirms the desired state, then $eval reads the element. For interactions, Puppeteer’s locator APIs provide automatic waiting for presence and an appropriate element state; the page-interactions guide describes when locators are preferable to lower-level handles.
Quick wins for a faster PC:
Scan for outdated or missing drivers - takes under a minuteDriver Scan →Repair Windows errors before they cause bigger problemsFix Now →Validate and build selectors safely
Reject invalid or empty input
function requireSelector(value) {
if (typeof value !== 'string' || value.trim() === '') {
throw new TypeError('selector must be a non-empty string');
}
return value;
}
async function hasElement(page, selector) {
const safeSelector = requireSelector(selector);
return (await page.$(safeSelector)) !== null;
}
Validation gives callers a clear error before Puppeteer reports a malformed query.
Escape dynamic text used inside a CSS selector
If a parameter is a value inside a selector rather than the complete selector, escape it before interpolation. Modern browsers expose CSS.escape() in page context:
async function findById(page, id) {
return page.evaluate(rawId => {
const escaped = CSS.escape(rawId);
return document.querySelector(`#${escaped}`)?.textContent ?? null;
}, id);
}
Prefer stable attributes such as data-testid. Avoid constructing selectors from untrusted input unless you validate or escape it; malformed selectors can fail the operation, and broad selectors can target the wrong element.
Rank #4
Remember that Puppeteer supports more than CSS
CSS examples use syntax such as .result, #login, and button[type="submit"]. Puppeteer also documents additional selector forms, including text, accessibility role/name, and XPath. Label a selector according to the syntax it uses so a function’s contract is not misleading.
Common mistakes and fixes
- Callback in the selector position:
page.$eval(() => ..., selector)is reversed. Usepage.$eval(selector, element => ...). - Literal variable name: replace
'selector'withselectorunless the literal word is genuinely the query. - Wrong callback parameter: in
$eval, the first callback parameter is the matched element. Inevaluate, parameters after the function receive the values you pass. - Unexpected null:
$does not wait. Check the URL, frame, selector spelling, and page timing, or wait first. - Unexpected exception:
$evalthrows for no match. Use$for optional elements or catch the specific failure. - Invalid selector: inspect punctuation, escaping, and quotes. Test the same string with
document.querySelector()in DevTools. - Wrong frame: selectors are scoped to a page or frame. Obtain the correct frame and call its selector methods there.
- Stale handle: a navigation or re-render can detach an
ElementHandle. Re-select after the DOM changes instead of retaining handles indefinitely.
Reliable reusable helper patterns
Return a default for an optional value
async function textOrDefault(page, selector, fallback = '') {
const node = await page.$(selector);
if (!node) return fallback;
try {
return await node.evaluate(element => element.textContent?.trim() ?? '');
} finally {
await node.dispose();
}
}
Wait, then click
async function clickWhenReady(page, selector) {
await page.waitForSelector(selector, { visible: true, timeout: 30_000 });
await page.click(selector);
}
For complex interactions, a locator can reduce manual waiting and handle state checks for you.
Performance, reliability, and debugging
- Pass one selector value through helpers rather than repeatedly rebuilding identical strings.
- Prefer one
$evalor$$evalthat returns the needed serializable data over transferring many element handles. - Use explicit, realistic timeouts. A long timeout can hide a broken selector; a short timeout can fail on a slow page.
- Log the final selector value, URL, frame URL, and timeout when diagnosing failures. Do not log secrets embedded in surrounding page data.
- After navigation, route changes, or framework re-renders, wait for a stable selector and then query again.
Or skip the browser setup
If your goal is a clean website image rather than DOM automation, ScreenshotNeo provides a single screenshot request. Before capture it accepts cookie or consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be disabled. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients.
See the complete parameter list in the ScreenshotNeo documentation. This cURL request saves a WebP image:
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,
)
r.raise_for_status()
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}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
await Bun.write('shot.webp', res);
ScreenshotNeo includes full-page and element capture, 12 device presets plus custom viewports, retina scale, dark mode, PDF controls, custom CSS and JavaScript, click and wait conditions, request blocking, headers, cookies, user agents, authorization, timezone and geolocation, transparent backgrounds, resizing, selectable cache TTLs, signed image links, asynchronous webhooks, bulk capture of up to 100 URLs per call, a usage API, and an OpenAPI specification. Its parameter names are compatible with those used by other screenshot APIs, which can simplify migration.
Do these 3 things before closing this tab:
1Fix the driver behind crashes, sound loss and screen glitches2Clear out junk files and repair common Windows errors3Scan for outdated or missing drivers - takes under a minuteThe Free plan includes 1,000 screenshots each month with no card. Paid plans start at $5 for 3,000 shots; all features are available on every plan, and yearly billing provides two months free. Sign up for the free ScreenshotNeo plan.
Best Value
Frequently Asked Questions
Does Puppeteer require a special syntax for a selector parameter?
No. A selector is an ordinary string. Pass the variable directly to the method that accepts a selector.
What is the difference between a selector argument and an evaluate argument?
In $eval, the selector is the first Puppeteer argument. In evaluate, values after the page function are delivered to that function as its parameters.
Which method should I use when an element may not exist?
Use page.$(selector) and test for null. Use $eval only when a match is required, or catch its no-match error.
Crashes, 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 minutePC 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 & 11Why does my selector work in DevTools but fail in Puppeteer?
Check that Puppeteer is querying the same frame and page state, that the selector is valid CSS, and that the element has appeared before the query runs.
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.

