Puppeteer evaluate() vs evaluateHandle(): What’s the Difference?
Use evaluate() for data you need in Node.js and evaluateHandle() when you need to retain a page-side object. Here are runnable examples, lifecycle details, and fixes for common errors.
page.evaluate() runs a function in the browser page and returns its result as a value that Node.js can use. page.evaluateHandle() runs a function in the same page context but returns a handle to the page-side object, so you can inspect it or pass it into another page function. Both methods wait for a promise returned by the function to resolve. Choose based on whether you need the value or need to keep the object reference.
The examples below follow Puppeteer 25.12.0. Check the official evaluate documentation and evaluateHandle documentation for the version installed in your project.
1. The difference at a glance
| Task | Method | What Node.js receives |
|---|---|---|
| Read a title, string, number, boolean, or data object | page.evaluate() |
The evaluated result as a value |
| Keep a reference to an object in the page | page.evaluateHandle() |
A JSHandle, or an ElementHandle for an element |
| Use an existing handle inside another page function | Either method can accept it as an argument | The handle remains a page-side reference |
As Puppeteer’s documentation puts it, “The only difference between page.evaluate and page.evaluateHandle is that evaluateHandle will return the value wrapped in an in-page object.” See the method documentation for the API details.
2. Runnable setup
Install Puppeteer in a new project. Its package installation supplies a compatible browser by default. If your environment manages Chrome separately, see Puppeteer’s installation guide for configuration.
npm init -y
npm install puppeteer
Save this as compare.mjs, then run node compare.mjs. The script demonstrates both methods on the same page.
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
await page.setContent(`
<!doctype html>
<html>
<head><title>Evaluation example</title></head>
<body><main><h1>Hello from the page</h1></main></body>
</html>
`);
// A plain string value comes back to Node.js.
const title = await page.evaluate(() => document.title);
console.log('title:', title);
// A handle keeps a reference to the page-side element.
const headingHandle = await page.evaluateHandle(() =>
document.querySelector('h1'),
);
try {
// Pass the handle into a page function to read a property there.
const headingText = await page.evaluate(
(heading) => heading.textContent,
headingHandle,
);
console.log('heading:', headingText);
} finally {
await headingHandle.dispose();
}
} finally {
await browser.close();
}
Expected output:
title: Evaluation example
heading: Hello from the page
3. Use evaluate() when you need data
Use evaluate() when the next step happens in Node.js: logging a result, storing it, comparing it, or returning it from your scraper. The function runs in the page, but Puppeteer returns the result across the page-to-Node boundary.
const pageData = await page.evaluate(() => ({
title: document.title,
heading: document.querySelector('h1')?.textContent?.trim() ?? null,
linkCount: document.querySelectorAll('a').length,
}));
console.log(pageData.title);
console.log(pageData.linkCount);
Return data the Node.js side can represent. A DOM node is not ordinary serialized data; if you need to keep and work with the node itself, use a handle instead. For nested results, return only the fields your application needs.
4. Use evaluateHandle() when you need a page-side reference
Use evaluateHandle() to retain a reference to an object in the page. This is useful when you want to pass the object into a later evaluation or use element-specific operations on a returned element.
const bodyHandle = await page.evaluateHandle(() => document.body);
try {
const childCount = await page.evaluate(
(body) => body.children.length,
bodyHandle,
);
console.log('body children:', childCount);
} finally {
await bodyHandle.dispose();
}
A handle is a Puppeteer object in Node.js that refers to a JavaScript object in the page. It is not the same as receiving that object as a plain Node.js value.
5. Returning elements and using ElementHandle
When the evaluated expression returns a DOM element, evaluateHandle() gives you an ElementHandle. That handle supports element operations such as click(). A selector-based API or locator may express a simple element lookup more directly, but evaluation handles are useful when the element is found through page-side logic.
import puppeteer from 'puppeteer';
import { ElementHandle } from 'puppeteer';
const button = await page.evaluateHandle<ElementHandle>(() =>
document.querySelector('button'),
);
try {
await button.click();
} finally {
await button.dispose();
}
The generic type shown is TypeScript syntax. In plain JavaScript, omit <ElementHandle>. Ensure the query actually finds an element before calling an element operation; a missing match can produce a null-related error.
6. Promises, arguments, and serialization
Both methods await returned promises
Promise support is not a reason to choose one method over the other. Puppeteer waits for the page function’s returned promise to resolve in either case.
const pageTitle = await page.evaluate(async () => {
await Promise.resolve();
return document.title;
});
const titleHandle = await page.evaluateHandle(async () => {
await Promise.resolve();
return document.querySelector('title');
});
Pass handles as arguments
Puppeteer can pass a handle into an evaluation function, as in the body and heading examples above. This lets the page-side function work with the referenced object without first turning it into a serialized value.
Extract serializable data with jsonValue()
Call jsonValue() if you want the serializable portions of a handle’s referenced value:
const detailsHandle = await page.evaluateHandle(() => ({
title: document.title,
url: location.href,
}));
try {
const details = await detailsHandle.jsonValue();
console.log(details);
} finally {
await detailsHandle.dispose();
}
jsonValue() does not call the referenced object’s toJSON() method. It can fail when circular references prevent serialization. If you only need plain data, returning a deliberately constructed object from evaluate() is often simpler.
7. Handle lifecycle and cleanup
Puppeteer handles keep their referenced page-side objects from being garbage-collected while the handle is live. Dispose a handle when you are done with it, especially inside loops or long-running browser sessions. Handles are also automatically disposed when their associated frame navigates away or their parent execution context is destroyed. Explicit disposal makes the intended lifetime clear and releases references sooner.
const handles = [];
try {
for (const selector of ['h1', 'main', 'footer']) {
const handle = await page.evaluateHandle((s) =>
document.querySelector(s), selector,
);
handles.push(handle);
}
// Use the handles here.
} finally {
await Promise.all(handles.map((handle) => handle.dispose()));
}
For simple reads, evaluate() avoids managing a handle lifecycle. For a handle that is needed across several page operations, keep it only as long as necessary and dispose it on both success and failure paths.
8. Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| The result is not a usable DOM node in Node.js | evaluate() returned a page object that cannot be treated as a live Node.js reference. |
Use evaluateHandle() to retain a page-side reference, or return specific serializable fields. |
Cannot read a property or call click() on null |
The selector did not match an element at evaluation time. | Check the selector and page state; wait for the element when it is created asynchronously, and handle the no-match case. |
| Serialization fails or data is incomplete | The result contains circular or otherwise non-serializable page objects. | Return a smaller plain object with the required fields, or keep a handle and avoid serializing the whole object. |
| A handle operation fails after navigation | The navigation replaced the frame’s execution context, which auto-disposes its handles. | Wait for navigation to finish, then query the page again and create a fresh handle. |
| Memory use grows in a long loop | Handles remain live because they were not disposed. | Dispose each handle in a finally block or batch cleanup with Promise.all. |
| Page function references a Node.js variable and gets an undefined value | Functions passed to evaluation run in the page context; they do not close over the Node.js lexical scope. | Pass needed values as arguments: page.evaluate((selector) => document.querySelector(selector), selector). |
9. Performance, reliability, and cost
For this choice, focus on what crosses the page-to-Node boundary and how long references remain alive. Return only the data needed by your application, and avoid keeping unnecessary handles. Both methods execute in the browser page and wait for returned promises; neither method makes a page function synchronous.
For reliable automation, account for page timing explicitly. A selector may not exist yet, and navigation can invalidate handles associated with the old frame. Wait for the relevant page state before evaluating, check for missing elements, and recreate handles after navigation when needed.
Puppeteer itself is an open-source browser automation library; the research sources do not specify a per-evaluation fee. Your practical costs come from the browser and infrastructure you run, such as compute, memory, and time spent maintaining browser processes. Keep browser sessions bounded and close pages and browsers when finished.
10. Capture a screenshot without managing a browser
If the task is to get an image of a website rather than inspect live page objects, ScreenshotNeo provides a website screenshot API and MCP server. Its API accepts one GET request for an image or PDF, so this path does not require you to set up Puppeteer and a browser for capture. See the ScreenshotNeo API documentation.
Or skip the browser setup
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)
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}`);
- Cookie banners are accepted and removed before capture; newsletter popups and chat widgets are removed too. Each cleanup step can be turned off.
- Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed. Response headers report the page verdict and billing status.
- An MCP server lets AI agents use screenshot, page-info, and PDF capture tools.
- The free plan includes 1,000 screenshots a month with no card. Paid plans start at $5 for 3,000 screenshots.
Create a free ScreenshotNeo account to get 1,000 screenshots a month with no card.
11. FAQ
Does evaluateHandle() always return an ElementHandle?
No. It returns a handle for the evaluated result. When that result is a DOM element, Puppeteer represents it as an ElementHandle; other objects are represented by a JSHandle.
Can I use evaluateHandle() to get a string?
Yes, but it returns a handle to the page-side result. If you only need the string in Node.js, use evaluate() to receive it directly as a value.
Do I need to dispose every handle?
Dispose handles when you finish with them. Navigation or destruction of the parent context also disposes them, but explicit cleanup is clearer and useful when the context stays alive.
Which method should I use for a screenshot?
Neither method is itself a screenshot API. Puppeteer can capture screenshots through its page screenshot API; if you only need a website image or PDF from a URL, ScreenshotNeo offers a direct API call described above.
