Use a Puppeteer locator and .filter() when you want to find an element and interact with it. If you specifically need persistent ElementHandle objects, query with page.$$() (or a container handle’s $$()) and test each returned handle in Node.js. If you only need text, attributes, or other values, use page.$$eval() instead.
Those approaches solve different problems: locators are the recommended starting point for interaction, handles let you retain references to page elements, and $$eval() returns data rather than handles. The examples below show how to choose, filter safely, and clean up handles.
Choose the right Puppeteer API
Start by deciding what you need after the selection. Puppeteer recommends locators for selecting elements to interact with; ElementHandle and waitForSelector are lower-level options for cases where locator functionality is not sufficient.
| What you need | Approach | What you get |
|---|---|---|
| Find a candidate and click, type, or otherwise interact with it | page.locator(selector).filter(predicate) |
A locator that can be used for an interaction. Locator actions can wait for relevant conditions. |
| Keep references to several matching elements | page.$$(selector), then evaluate a predicate for each handle |
An array of ElementHandle objects you must manage and dispose. |
| Read text, attributes, or other serializable values | page.$$eval(selector, callback) |
The callback’s returned value, not a persistent collection of handles. |
| Run custom page-side selection and retain its result | page.evaluateHandle(callback) |
A handle to the returned page object; dispose it when finished. |
Prefer a selector or locator when it expresses the selection clearly. Puppeteer’s selector syntax includes CSS and additional forms such as text, accessibility selectors, XPath, and open Shadow DOM traversal; consult the guide for the syntax supported by your installed version.
#1 Best Overall
Filter a locator and interact with the match
For a button whose text must match exactly, use a locator filter. The predicate runs in the browser page context, so it can inspect the candidate element but cannot directly access ordinary Node.js variables.
await page
.locator('button')
.filter(button => button.textContent === 'My button')
.click();
Locator actions can automatically wait for conditions relevant to the action. For clicking, Puppeteer’s guide describes checks involving viewport position, visibility, enabled state, and bounding-box stability. This can make a locator a better fit than immediately collecting handles when the actual goal is simply to click the right button.
Use a dynamic value in a locator predicate
To compare against a value defined in Node.js, serialize it into the predicate function string rather than expecting a callback to close over the local variable:
const buttonName = 'My button';
await page
.locator('button')
.filter(`button => button.textContent === ${JSON.stringify(buttonName)}`)
.click();
JSON.stringify() produces a JavaScript string literal for this example, including escaping characters that could otherwise break the generated expression. Keep the value as data; do not build a predicate by concatenating untrusted code.
The Tool Desk
Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Rank #2
- HTML CSS Design and Build Web Sites
- Comes with secure packaging
- It can be a gift option
Get and filter actual ElementHandles
Use page.$$() when you need an array of references to every element matching a selector. It returns an array of ElementHandle objects. Evaluate each candidate in the page context, passing the expected value as an argument, and retain only the matches:
const handles = await page.$$('button');
const matchingHandles = [];
for (const handle of handles) {
const matches = await handle.evaluate(
(button, expectedName) => button.textContent === expectedName,
'My button',
);
if (matches) {
matchingHandles.push(handle);
} else {
await handle.dispose();
}
}
// matchingHandles now contains the matching ElementHandles.
// Use them for the work that requires retained element references.
for (const handle of matchingHandles) {
await handle.click();
await handle.dispose();
}
The callback passed to handle.evaluate() runs against the element in the browser context. The expected label is supplied as an argument, so it does not rely on a Node.js closure. In this example, the exact comparison deliberately treats whitespace and letter case as significant. If the page contains extra whitespace, choose an explicit normalization rule, such as trimming the text, and use that same rule for the expected label.
Dispose of non-matches and matches
An ElementHandle keeps its DOM element from being garbage-collected while the handle remains active. Dispose of rejected handles as soon as you know they are not needed, and dispose of retained handles when their work is complete. The loop above disposes rejected handles immediately; its final loop disposes each retained handle after clicking it.
If an operation can fail after some handles have been collected, put cleanup in a finally block so retained references are released even when an action throws:
Do these 3 things before closing this tab:
1Scan for outdated or missing drivers - takes under a minute2Repair Windows errors before they cause bigger problems3Fix the driver behind crashes, sound loss and screen glitchesRank #3
const handles = await page.$$('button');
const matchingHandles = [];
try {
for (const handle of handles) {
const matches = await handle.evaluate(
(button, expectedName) => button.textContent === expectedName,
'My button',
);
if (matches) {
matchingHandles.push(handle);
} else {
await handle.dispose();
}
}
for (const handle of matchingHandles) {
await handle.click();
}
} finally {
await Promise.all(
matchingHandles.map(handle => handle.dispose()),
);
}
This cleanup assumes the rejected handles were already disposed in the loop. If you add code that may throw before all candidates have been classified, track every acquired handle and ensure the cleanup path covers those that remain undisposed. A navigation or destruction of the parent execution context also causes automatic disposal, but explicit cleanup makes the intended lifetime clear.
Scope a query to a container
If the target buttons belong to one known region, first locate that region and query within its handle. The scoped query avoids collecting matching buttons elsewhere on the page:
const container = await page.$('#results');
if (!container) {
throw new Error('Results container was not found');
}
const buttons = await container.$$('button');
for (const button of buttons) {
// Evaluate a predicate or use the handle here.
await button.dispose();
}
await container.dispose();
Check for a null container before calling its query method. Each returned button handle and the container handle have separate lifetimes, so dispose of both when they are no longer needed.
Return values instead of handles with $$eval
If the goal is to collect labels, there is no need to retain element references. page.$$eval() passes all matching DOM nodes to a callback in the page context and resolves to the callback’s result:
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 minuteRank #4
- Brand: Wiley
- Set of 2 Volumes
- A handy two-book set that uniquely combines related technologies Highly visual format and accessible language makes these books highly effective learning tools Perfect for beginning web designers and front-end developers
const labels = await page.$$eval('button', buttons =>
buttons
.filter(button => button.textContent === 'My button')
.map(button => button.textContent),
);
console.log(labels);
The returned result should be data that can be passed back from the page context, such as strings or arrays of values. It is not an array of durable ElementHandle objects. Use this approach for extraction; use locator interaction or handles when you must act on the actual page elements.
Get a handle from a custom selection
When a custom page-side expression selects one element and you need to keep an element reference, use page.evaluateHandle(). For example:
const button = await page.evaluateHandle(() =>
document.querySelector('button'),
);
try {
await button.click();
} finally {
await button.dispose();
}
When the in-page callback returns an element reference, Puppeteer provides an ElementHandle. By contrast, page.evaluate() returns the evaluated value and is appropriate when you want data rather than a retained page object. In TypeScript, the API reference notes that a generic can be supplied when you know the return value is an ElementHandle.
Common problems and fixes
- The filter cannot see a Node.js variable: locator filters execute in the browser context. Embed a serialized value in the function string, as shown above, or pass values as arguments to an evaluation callback.
- The selected text does not match: exact string comparison distinguishes whitespace and case. Inspect the actual text, then choose whether to compare exactly, trim whitespace, or use another deliberate matching rule.
- The container lookup is null: the selector did not find a matching element at the time of the query. Check the selector and page state, and handle the null result before calling
container.$$(). - A handle is detached or its context is gone: the page may have replaced the node, navigated, or destroyed the execution context between selection and action. Re-query against the current page state; for ordinary interaction, consider a locator that can wait for action conditions.
- Handles accumulate during a long task: dispose of non-matches immediately and dispose of matches after use. A retained handle keeps a page object alive until it is disposed or its parent context is destroyed.
- You only need text or attributes: use
$$eval()and return the data. Keeping handles adds lifecycle work without providing a benefit if no later action needs the elements.
Version and behavior notes
Puppeteer’s official documentation pages identified for these APIs carry different version labels: the interactions guide and Page evaluation references are labeled 25.12.0, the ElementHandle class reference 25.10.0, and the $$ and $$eval references 25.9.0. Those labels are not a single shared version guarantee. Check the documentation corresponding to the Puppeteer version installed in your project before relying on version-specific details.
Best Value
Or skip the browser setup
If your goal is a screenshot rather than inspecting or interacting with individual DOM elements, ScreenshotNeo takes a screenshot through one GET request. It can accept cookie or consent banners and remove known consent platforms, newsletter popups, and chat widgets before capture; those steps 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. An MCP server provides screenshot tools for AI agents and other MCP clients.
For example, with cURL:
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. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Sign up for the free plan.
Frequently Asked Questions
Does page.$$() return ElementHandles or DOM nodes?
It returns an array of Puppeteer ElementHandle objects for elements matching the selector.
Can I pass a Node.js variable directly into a locator filter callback?
No. The callback runs in the page context. Serialize a value into a function string or pass it as an argument to an evaluation callback.
Recommended Free Tools
What should I use if I need to keep only one result from a custom page-side query?
Use evaluateHandle() for the returned page object, then dispose of the resulting handle when finished.
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.

