How to Evaluate JavaScript on a Puppeteer Page
Run JavaScript in Puppeteer's page context, pass data safely, handle async results and DOM references, and choose the right evaluation method.
Use await page.evaluate(pageFunction, ...args) to run JavaScript in the browser page and return its result to your Puppeteer script. The callback runs in the page context, so it cannot read Node.js variables from the surrounding scope; pass values as arguments. Use evaluateHandle when you need to keep a DOM object by reference, $eval for one matched element, and evaluateOnNewDocument for setup that must run before page scripts.
1. Set up Puppeteer
Install Puppeteer in a Node.js project. The package can download a compatible browser for its default setup.
npm install puppeteer
Save this as evaluate.mjs and run it with node evaluate.mjs. Replace the example URL with a page you are allowed to access.
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 title = await page.evaluate(() => document.title);
console.log(title);
} finally {
await browser.close();
}
The result of page.evaluate is the value returned by the callback. Puppeteer serializes the callback, executes it in the page, and returns a serializable result to Node.js. See the official JavaScript execution guide and Page.evaluate API.
2. Pass values into the page context
A callback passed to evaluate does not retain closures from your Node.js script. This fails because suffix exists only in Node.js:
const suffix = ' — inspected';
const title = await page.evaluate(() => document.title + suffix);
Pass the value as an argument instead. Arguments follow the callback and become its positional parameters:
const suffix = ' — inspected';
const title = await page.evaluate(
suffix => document.title + suffix,
suffix,
);
console.log(title);
For multiple inputs, pass multiple arguments. Keep the page-side logic inside the callback, including any helper functions it needs.
const title = await page.evaluate(
(prefix, suffix) => `${prefix}${document.title}${suffix}`,
'Title: ',
' — inspected',
);
Use values that can be transferred through Puppeteer’s evaluation protocol. A JSHandle can also be supplied as an argument when you already hold a reference to an in-page object.
3. Return values and await page-side work
Return strings, numbers, booleans, arrays, and plain data objects when the result needs to go back to Node.js. Both the call to page.evaluate and the page callback’s asynchronous work should be awaited.
const pageData = await page.evaluate(async () => {
const response = await fetch('/data.json');
const data = await response.json();
return {
title: document.title,
itemCount: Array.isArray(data.items) ? data.items.length : 0,
};
});
console.log(pageData);
If the callback returns a Promise, Puppeteer waits for it to resolve and returns its value. That only waits for the work represented by that Promise; it does not mean the page has reached an application-specific state. For that, wait for the relevant selector, navigation, or other condition before evaluating.
For example, if a page updates a status element after loading, wait for it explicitly:
await page.waitForSelector('[data-state="ready"]');
const status = await page.evaluate(() =>
document.querySelector('[data-state="ready"]')?.textContent?.trim() ?? null
);
A browser-side fetch also follows the page’s origin and browser security rules. If the request is blocked by CORS, authentication, or the site’s content security policy, evaluating it does not bypass those protections.
4. Choose between evaluate, handles, selectors, and early setup
| Need | Method | What comes back |
|---|---|---|
| Compute a value from the current page | page.evaluate(fn, ...args) |
A serialized result; a returned Promise is awaited. |
| Keep a page object or DOM node for later work | page.evaluateHandle(fn, ...args) |
A JSHandle, or an ElementHandle for an element. |
| Run a callback on the first matching element | page.$eval(selector, fn, ...args) |
The callback’s result; throws when there is no matching element. |
| Install code before the page’s scripts execute | page.evaluateOnNewDocument(fn, ...args) |
A registration handle; the function runs in each qualifying new document. |
These methods differ by return semantics, target, and timing. Use ordinary evaluation for data extraction or computation, handles for retained references, selector evaluation for a known element, and new-document evaluation for early setup.
Keep a DOM node with evaluateHandle
Returning a DOM node through ordinary evaluate does not transfer a live browser DOM object to Node.js. For reference-based work, use evaluateHandle, then dispose of the handle when finished.
const bodyHandle = await page.evaluateHandle(() => document.body);
try {
const html = await bodyHandle.evaluate(element => element.innerHTML);
console.log(html);
} finally {
await bodyHandle.dispose();
}
Handles retain references to objects in the page. Dispose of handles you no longer need; navigation or destruction of the execution context can also dispose of them. See the evaluateHandle API and JSHandle API.
Use $eval for one element
$eval finds the first element matching a selector and passes that element as the callback’s first argument. It throws if no element matches, so account for optional elements or wait when the element is expected to appear asynchronously.
const heading = await page.$eval('h1', element => element.textContent?.trim() ?? '');
console.log(heading);
When the element may not exist, use a selector wait before calling $eval, or query it with evaluate and return a nullable value:
const heading = await page.evaluate(() =>
document.querySelector('h1')?.textContent?.trim() ?? null
);
See Page.$eval for the selector evaluation behavior.
Run setup before site scripts
Register a function with evaluateOnNewDocument when it must run after a document is created but before that document’s scripts execute. It also applies to navigation and qualifying child-frame attachment or navigation events.
await page.evaluateOnNewDocument(() => {
// This runs in the new document before its scripts execute.
window.name = 'automation-session';
});
await page.goto('https://example.com');
Register before navigating if the initial document needs the setup. This is not a way to evaluate against the current document after its scripts have already run. Check the evaluateOnNewDocument API for the current version’s exact behavior.
5. Troubleshoot common evaluation errors
| Symptom | Likely cause | Fix |
|---|---|---|
ReferenceError for a Node variable inside the callback |
The callback runs in the page context and cannot close over Node.js lexical scope. | Pass the variable after the callback and accept it as a parameter. |
A returned DOM node looks empty or becomes {} |
Ordinary evaluation serializes the result; it does not return a live Node.js DOM object. | Use evaluateHandle and dispose the handle when finished. |
$eval reports that no element was found |
The selector did not match at evaluation time. | Check the selector, wait for the element when appropriate, or return a nullable result from evaluate. |
| The result is missing or appears to be a Promise | The outer Puppeteer call was not awaited, or the callback did not return the value you expected. | Use await page.evaluate(...) and return the desired result from the callback. |
| A handle cannot be used after navigation | Navigation can destroy its execution context and invalidate page references. | Acquire a fresh handle in the current document and dispose of old handles where possible. |
| TypeScript accepts code that fails in the browser | Node-side types do not guarantee that globals, packages, or runtime APIs exist in the page. | Use browser-supported APIs and define all needed page-side logic inside the callback. |
| Evaluation fails because the page or frame navigated | The target execution context changed while the operation was running. | Wait for the intended navigation or page state, then evaluate in the resulting document. |
6. Reliability, performance, and cost
- Keep evaluation focused. Return only the data the Node.js process needs instead of transferring large page structures or repeatedly serializing the same content.
- Wait for a real condition. A Promise inside the callback waits for that Promise, not for unrelated client-side rendering. Prefer a selector or state condition that represents readiness over an arbitrary delay.
- Expect document changes. Navigation, frame replacement, and page closure can invalidate handles or interrupt evaluation. Acquire references after the target document is ready and handle failures around navigation boundaries.
- Dispose retained references. Long-lived handles keep in-page objects reachable until disposal or execution-context destruction.
- Account for browser costs. Local Puppeteer work consumes the resources needed to run the browser and page. Reuse a browser for multiple pages where your process design allows it, and close pages and the browser when finished.
- Respect page behavior. Page-side code runs with the page’s browser permissions and policies; evaluation is not a shortcut around authentication, CORS, or site access controls.
7. Or skip the browser setup
If your goal is a screenshot rather than executing custom page logic, ScreenshotNeo takes a screenshot with one GET request. It is a website screenshot API and MCP server; its options include custom JavaScript and CSS when a capture needs page adjustments. See the ScreenshotNeo API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);
Cookie banners are accepted like a visitor and removed along with 60+ known consent platforms, newsletter popups, and chat widgets before the shot; each step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits cost nothing, and response headers report the page verdict and billing status. An MCP server gives AI agents tools to take screenshots, get page information, and capture PDFs. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account to get started.
8. FAQ
Can evaluate return an async result?
Yes. Return a Promise from the callback and await page.evaluate; Puppeteer resolves the Promise and returns its value.
Can I use a function declared in my Node.js file inside evaluate?
Not by closure. The callback is serialized for the page context. Put required logic in the callback or pass its input data explicitly.
When should I use evaluateHandle instead of evaluate?
Use a handle when later operations need the original in-page object or element by reference. Use evaluate when a serializable value is enough.
Does evaluateOnNewDocument run on the current page immediately?
It registers code for new documents before their page scripts execute. Register it before navigation when the destination document needs the setup.


