Quick wins for a faster PC:
Clear out junk files and repair common Windows errorsFree Scan →Scan for outdated or missing drivers - takes under a minuteDriver Scan →Repair Windows errors before they cause bigger problemsFix Now →Pass a function to page.evaluate(), then pass any Node.js values it needs as arguments after the function. Puppeteer serializes the callback, runs it in the browser page context, waits for a returned Promise, and sends a serializable result back to Node.js.
For example, the callback below reads the page title and appends a suffix defined in Node.js:
const suffix = ' — product page';
const title = await page.evaluate(
suffixFromNode => document.title + suffixFromNode,
suffix,
);
The key is to treat the callback as a separate browser-side function: use browser APIs such as document inside it, and pass in the values it needs explicitly. If you need a screenshot rather than browser-side evaluation, see the optional ScreenshotNeo alternative below.
How page.evaluate() works
page.evaluate() accepts a function first and then zero or more arguments for that function. The callback runs in the browser page context, not as ordinary code in your Node.js context. Puppeteer’s API describes it as evaluating a function in the page’s context and returning the result.
Free tools Windows power users keep installed
One-click scans. No signup required.
#1 Best Overall
That context boundary explains the most common surprise: a callback does not automatically have access to local variables surrounding the call. Its parameters receive the values you explicitly pass after the callback.
const selector = 'h1';
const heading = await page.evaluate(
selectorFromNode => document.querySelector(selectorFromNode)?.textContent?.trim() ?? null,
selector,
);
Here selector is defined in Node.js. The callback receives its value as selectorFromNode, then uses the browser’s document to find the matching element. The optional chaining handles the case where no matching heading exists.
Pass Node.js values as callback arguments
Put the callback first and its arguments afterward. The callback’s parameters are assigned values in the same order. This works for strings, numbers, booleans, arrays, and ordinary objects that can be serialized across the browser protocol boundary.
Pass multiple values
const selector = 'a.product';
const limit = 10;
const products = await page.evaluate(
(selectorFromNode, limitFromNode) =>
Array.from(document.querySelectorAll(selectorFromNode))
.slice(0, limitFromNode)
.map(node => ({
text: node.textContent?.trim() ?? '',
href: node.href ?? null,
})),
selector,
limit,
);
This returns up to ten matching links as plain objects, rather than returning browser DOM nodes. Each object contains text and an absolute link URL, or null if that property is unavailable.
Rank #2
Pass related values in one object
If a callback needs several related options, one object can keep the call readable. Destructure it in the callback:
const result = await page.evaluate(
({ selector, limit }) => {
return Array.from(document.querySelectorAll(selector))
.slice(0, limit)
.map(node => ({
text: node.textContent?.trim() ?? '',
href: node.href ?? null,
}));
},
{ selector: 'a.product', limit: 10 },
);
The object is still passed as data; it does not give the callback access to the Node.js scope. Keep arguments to values that can cross the protocol boundary. For a live browser object, use a supported handle instead of trying to serialize a DOM node.
Why Node.js variables are not available inside the callback
A callback passed to page.evaluate() is serialized and evaluated in the page. It is not executed as a closure that retains all the variables around its original definition. Code like this therefore should not rely on suffix being captured from Node.js:
const suffix = ' — product page';
const title = await page.evaluate(() => document.title + suffix);
Pass the value explicitly instead:
const suffix = ' — product page';
const title = await page.evaluate(
suffixFromNode => document.title + suffixFromNode,
suffix,
);
Puppeteer’s troubleshooting guidance notes that functions are serialized using Function.prototype.toString(); transpilers can transform functions in ways that make the serialized output incompatible. If an otherwise simple callback fails after a build or transpilation step, check the function that reaches Puppeteer, not just its original source form.
Use async callbacks when browser-side work is asynchronous
page.evaluate() waits if the callback returns a Promise, so it can be declared async. The value delivered to Node.js is the resolved result:
const price = await page.evaluate(async () => {
const response = await fetch('/api/price');
const data = await response.json();
return data.current;
});
The callback’s await expressions run in the page context, and the outer Node.js call must also be awaited if you need the result before continuing. Returning a Promise without awaiting it yourself is also supported by Puppeteer, which waits for that Promise to resolve.
This promise behavior applies to work represented by the callback’s returned Promise. Do not treat page.evaluate() as a general wait for unrelated page activity: make the callback explicitly await the asynchronous operation whose result it needs.
Return data, not live DOM nodes
The result of page.evaluate() must be transferable back as data. Strings, numbers, arrays, and plain objects are useful return values. A DOM node or function is not a way to transfer a live browser object to Node.js; a non-serializable result resolves to undefined.
Recommended Free Tools
Rank #4
Instead of returning each element, extract the information you need while still in the page:
const cards = await page.evaluate(() =>
Array.from(document.querySelectorAll('.card')).map(card => ({
title: card.querySelector('h2')?.textContent?.trim() ?? null,
url: card.querySelector('a')?.href ?? null,
})),
);
Use page.evaluateHandle() when you need to retain a wrapper for an in-page object and perform further operations against it. Dispose of a handle when you no longer need it. This is different from page.evaluate(), which is for returning a value as data.
Choose between evaluate, $eval, $$eval, and evaluateHandle
| API | Selector behavior | What the callback receives | Result | Async callback |
|---|---|---|---|---|
page.evaluate() |
No selector is required; the callback can query the page itself. | Only the arguments you pass after the callback. | Data returned from the page context. | Returned Promises are awaited. |
page.$eval() |
Uses a selector to find one element. | The matched element, followed by any additional arguments. | Data returned from the callback. | Returned Promises are awaited. |
page.$$eval() |
Uses a selector to find matching elements. | An array of matched elements, followed by any additional arguments. | Data returned from the callback. | Returned Promises are awaited. |
page.evaluateHandle() |
No selector is required; the callback can work with page objects. | The arguments you pass to the callback. | A handle to an in-page object rather than copied data. | Use it when a retained in-page object is needed. |
Use $eval for one selected element
const inputValue = await page.$eval('#email', input => input.value);
This is more direct than querying for one element manually inside a general page.evaluate() callback.
Use $$eval to process a group
const labels = await page.$$eval(
'label',
nodes => nodes.map(node => node.textContent?.trim() ?? ''),
);
The callback gets the matched elements as an array, making it convenient to map them into serializable output. Both selector shortcuts accept additional callback arguments and await returned Promises.
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 →Best Value
TypeScript: annotate element types when inference is too broad
Puppeteer’s current API signatures model page.evaluate() as a generic function whose parameters are inferred from the callback and whose result is a Promise of the callback’s awaited return type. For selector helpers, TypeScript may infer a general Element or Element[]. If you need element-specific properties, annotate the callback parameter with the appropriate DOM subtype:
const value = await page.$eval(
'#email',
(el: HTMLInputElement) => el.value,
);
That annotation tells TypeScript the selected element is an input, so value is available in the callback’s type. Use the element subtype that actually matches your selector; a type annotation does not change what the page contains.
Troubleshooting common page.evaluate() problems
- A Node.js variable is undefined in the callback: pass it after the callback and receive it through a parameter. Do not rely on closure capture across the browser context.
- The result is
undefinedeven though the callback ran: check whether the callback returned a DOM node, a function, or another value that cannot be serialized. Return the fields you need as plain data, or useevaluateHandle()for a retained object. - An async result arrives too early or is missing: return or await the Promise from the callback, and make sure the Node.js call itself is awaited. The evaluator waits for a Promise the callback returns; it cannot infer unrelated page work you meant to wait for.
- The callback fails after transpilation: inspect the serialized function. Puppeteer uses
Function.prototype.toString(), and transpiler output can be incompatible with evaluation. - TypeScript rejects a DOM property: annotate the selected element with a suitable subtype, such as
HTMLInputElement, when inference gives onlyElement.
Or skip the browser setup
If the goal is to capture a website image or PDF, rather than run custom browser-side logic, ScreenshotNeo offers a one-request screenshot API and an MCP server for AI agents. It is not a replacement for page.evaluate() when you need arbitrary JavaScript evaluation.
cURL example (see the ScreenshotNeo API documentation for options):
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
ScreenshotNeo accepts cookie or consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each of those steps can be turned off. Bot checks or CAPTCHAs, 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 exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients. The Free plan includes 1,000 shots per month without a card; paid plans start at $5 for 3,000 shots, and all features are on every plan.
Sign up for ScreenshotNeo’s free plan to get 1,000 screenshots a month with no card.
Frequently Asked Questions
Can I use page.evaluate() to change the page?
Yes. Its callback runs in the page context, so it can use browser APIs to read or modify the document. Its return value still needs to be serializable if you want to receive it as data in Node.js.
Can page.evaluate() return a function for Node.js to call later?
No. A function is not transferred as an ordinary serializable return value. Use a handle for a live in-page object, or return data produced by the callback.
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.

