ScreenshotNeo

BlogHow-to

How to Evaluate JavaScript on a Puppeteer Element

Use ElementHandle.evaluate() to run page-context JavaScript on a Puppeteer element. Compare it with page.evaluate(), $eval(), and $$eval(), with runnable examples and fixes for common errors.

By the ScreenshotNeo team4 October 20269 min read

To evaluate JavaScript on an element you already have as a Puppeteer ElementHandle, call element.evaluate(fn). Puppeteer passes the element to fn as its first argument, runs that function in the page context, and returns its result to Node.js:

const element = await page.$('h1');
if (!element) throw new Error('Heading not found');

const text = await element.evaluate(el => el.textContent);
console.log(text);
await element.dispose();

You can also pass the handle to page.evaluate(fn, element). Use element.$eval() or element.$$eval() when you want to select descendants within that element. Puppeteer waits for a promise returned by the page function to resolve. See the official JavaScript execution guide and page interactions guide.

1. Choose the right evaluation method

Method Use it when What the callback receives
element.evaluate(fn, ...args) You already have an element handle and want to compute something from that element. The element is the first argument, followed by any explicit arguments.
page.evaluate(fn, element, ...args) You want page-level code but need to supply an existing element handle. The handle resolves to its page-side element object.
element.$eval(selector, fn, ...args) You need the first matching descendant of a known element. The matching descendant is the first argument.
element.$$eval(selector, fn, ...args) You need to compute a result from every matching descendant. An array of matching elements is the first argument.
page.$eval(selector, fn, ...args) You need the first page-level match and its result in one call. The first page-level match is the first argument.
page.$$eval(selector, fn, ...args) You need results for all page-level matches in one call. An array of page-level matches is the first argument.
page.locator(selector) You need to select and interact with an element, such as clicking or filling a field. A locator manages selection and waits for action preconditions; it is not a replacement for arbitrary element computation.

The ElementHandle.evaluate API, ElementHandle.$eval API, and ElementHandle.$$eval API document the handle-level operations. For ordinary interaction, Puppeteer recommends locators, which wait for conditions such as presence and readiness before acting.

2. Run a complete Puppeteer example

This runnable Node.js example opens a page, selects a heading, evaluates its text and attributes, then disposes the handle and closes the browser even if an error occurs. Install Puppeteer in a project with npm install puppeteer, save this as evaluate-element.mjs, and run it with node evaluate-element.mjs.

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({ headless: true });
try {
  const page = await browser.newPage();
  await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });

  const heading = await page.$('h1');
  if (!heading) {
    throw new Error('No h1 element found');
  }

  try {
    const result = await heading.evaluate(el => ({
      text: el.textContent?.trim() ?? '',
      tag: el.tagName.toLowerCase(),
      id: el.id,
      visible: el.getClientRects().length > 0,
    }));
    console.log(result);
  } finally {
    await heading.dispose();
  }
} finally {
  await browser.close();
}

The callback is serialized and executed in the browser page context. It can use the element and browser APIs, but it cannot close over variables from your Node.js module. Values needed by the callback must be passed as arguments.

3. Pass values into the page function

Pass additional values after the callback. The element remains the first argument to ElementHandle.evaluate():

const heading = await page.$('h1');
if (!heading) throw new Error('Heading not found');

const prefix = 'Page heading: ';
const text = await heading.evaluate((el, prefix) => {
  return prefix + (el.textContent?.trim() ?? '');
}, prefix);

console.log(text);
await heading.dispose();

At page level, provide the element handle and other arguments after the callback:

const details = await page.evaluate((el, attributeName) => ({
  text: el.textContent?.trim() ?? '',
  value: el.getAttribute(attributeName),
}), heading, 'aria-label');

Do not expect ordinary Node.js variables, imported modules, or functions to exist inside the callback. Pass serializable inputs explicitly and return serializable output.

4. Evaluate descendants or multiple matches

Element-scoped selectors let you work within a known container instead of querying the entire page. $eval evaluates against the first matching descendant and throws if it does not exist:

const card = await page.$('.product-card');
if (!card) throw new Error('Product card not found');

const title = await card.$eval('.title', node => node.textContent?.trim() ?? '');
await card.dispose();

Use $$eval to map all matching descendants into data that can be returned to Node.js:

const section = await page.$('main');
if (!section) throw new Error('Main section not found');

const titles = await section.$$eval('.title', nodes =>
  nodes.map(node => node.textContent?.trim() ?? '')
);
console.log(titles);
await section.dispose();

The callback runs once with the array of matching nodes, so return plain data such as strings or objects rather than trying to return browser DOM nodes as ordinary Node.js values. See the official $eval API and $$eval API.

5. Understand results, promises, and handles

Return values

evaluate() resolves to the callback’s result. Strings, numbers, booleans, arrays, and plain objects are common choices. DOM objects are not generally useful as ordinary serialized results; return the properties you need, or use a handle API if you need to keep a reference to an in-page object.

Asynchronous page functions

If the callback returns a promise, Puppeteer waits for it. For example:

const label = await element.evaluate(async el => {
  await new Promise(resolve => setTimeout(resolve, 50));
  return el.getAttribute('aria-label');
});

This waits for the promise created inside the page. It does not automatically wait for a selector to appear before you acquire the handle. Wait for or locate the element separately when needed.

Use evaluateHandle for a retained page object

Use evaluateHandle() when the result should remain a reference to an object in the page rather than being serialized into a value. For example, to retain a descendant element:

const childHandle = await element.evaluateHandle(el => el.querySelector('.child'));
try {
  const childText = await childHandle.evaluate(child => child?.textContent?.trim() ?? null);
  console.log(childText);
} finally {
  await childHandle.dispose();
}

For a known element result, Puppeteer’s TypeScript types may require an appropriate handle type or narrowing. See Page.evaluateHandle. Dispose handles acquired explicitly when you no longer need them. Handles are also disposed when their frame navigates or their execution context is destroyed.

6. Handle missing elements and page state

page.$() returns null when there is no match. Check it before calling methods on the result. By contrast, page.$eval() and the corresponding scoped $eval() throw when the selector has no match. The page interactions guide describes querying without waiting and locator-based interaction.

If content is added after navigation by client-side JavaScript, use a locator or an explicit wait before querying. For example:

await page.locator('h1').wait();
const heading = await page.$('h1');
if (!heading) throw new Error('Heading disappeared after waiting');
const text = await heading.evaluate(el => el.textContent?.trim() ?? '');
await heading.dispose();

A handle belongs to the frame and execution context where it was created. Navigation can invalidate it. If the page navigates, query again in the new document rather than reusing an old handle.

7. Common errors and fixes

Symptom Likely cause Fix
Cannot read properties of null page.$() found no match, or an optional descendant was absent. Check the returned handle before calling evaluate(); for an optional child, return a null-safe result or handle its absence explicitly.
failed to find element matching selector A $eval() selector did not match any element. Verify the selector and page state, wait for client-rendered content if necessary, or use $() when you want to handle absence without an exception.
The callback cannot see a Node.js variable The callback runs in the page context and does not capture the Node.js lexical scope. Pass the value as an explicit argument after the callback.
The result is undefined or not usable in Node.js The callback did not return a value, or it returned a page object where plain data was expected. Return an explicit serializable value; use evaluateHandle() if you need a retained page-side reference.
Execution context was destroyed or a detached handle error The page navigated, the frame changed, or the target node was removed before evaluation. Wait for the relevant navigation or rendering to finish, then query a fresh handle in the current frame. Avoid keeping handles across navigation.
A click or fill races with the page Evaluation is being used for an ordinary interaction without readiness checks. Prefer page.locator(selector).click() or .fill(value); locators wait for action preconditions and retry when appropriate.
Memory or protocol work grows over many iterations Explicitly acquired handles are retained after they are no longer useful. Dispose handles in a finally block. Prefer $eval/$$eval for one-off extraction.

8. Performance and reliability

  • Keep work in one evaluation: when extracting many fields from one element, return one object rather than making a separate browser round trip for each property.
  • Use bulk evaluation for lists: $$eval() can map many matching nodes in one callback and return compact data.
  • Return only what Node.js needs: large result objects add serialization and transfer work. Avoid returning full markup when a few fields suffice.
  • Wait for the right condition: navigation completion does not guarantee that application data has rendered. Wait for a selector or meaningful state before evaluating.
  • Account for races: a selected node can be removed or replaced between selection and evaluation. Keep the interval small and reacquire the element after rerendering.
  • Clean up handles: explicitly dispose acquired handles once finished, particularly in loops and long-lived browser sessions.
  • Keep callbacks self-contained: page callbacks are sent for execution in the browser context. Use browser-supported APIs and explicit parameters instead of relying on Node.js scope.

Evaluation itself does not incur a ScreenshotNeo charge or require a screenshot service; it is Puppeteer’s browser automation API. When the goal is to capture a visual result rather than compute DOM data, choose a screenshot workflow and account for browser launch, navigation, readiness, image size, and retries in your own system.

9. Or skip the browser setup

If you need a screenshot of the rendered page instead of a DOM value, ScreenshotNeo is a website screenshot API and MCP server. One GET request returns an image or PDF, without launching and maintaining a Puppeteer browser in your application. See the ScreenshotNeo API documentation.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
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)
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 request failed: ${res.status}`);
const bytes = new Uint8Array(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', bytes));

Cookie and consent banners, newsletter popups, and chat widgets are removed before the shot, and each step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed; response headers identify the page verdict and billing status. AI agents can use the MCP server’s take_screenshot, get_page_info, and capture_pdf tools. The free plan includes 1,000 screenshots per month with no card, and paid plans start at $5 for 3,000. Sign up for free and get 1,000 screenshots a month with no card.

10. Frequently asked questions

Can I use a CSS selector directly without first getting a handle?

Yes. Use page.$eval(selector, fn) for the first match or page.$$eval(selector, fn) for all matches. These are convenient when the result is needed immediately.

Does evaluate run JavaScript in Node.js?

No. Puppeteer runs the callback in the page’s browser context. The returned value becomes available to the Node.js caller.

Should I use evaluate to click an element?

Usually use a locator for ordinary interaction. It can wait for the element to be ready and perform the action with built-in checks. Use evaluation when you need custom computation on page objects.

When should I call dispose()?

Call it when you obtained a handle with a query or handle-returning method and have finished using that reference. One-off calls such as page.$eval() return a value rather than an element handle to dispose.