How to Work with JavaScript Handles in Puppeteer
Learn when to use Puppeteer’s JSHandle and ElementHandle, how to pass and inspect page objects, and how to release handles safely.
A Puppeteer JavaScript handle is a live reference to an object in the page’s JavaScript context. Use page.evaluate() when you want a serializable value in Node.js; use page.evaluateHandle() when you need to keep working with the page-side object. A DOM element returned as a handle becomes an ElementHandle, which provides element-specific operations.
This guide targets the Puppeteer 25 API shape shown in the official references. Those references include several 25.x releases; check the documentation for the version installed in your project if you rely on a particular TypeScript overload. The examples use JavaScript ES modules and assume Node.js and Puppeteer are installed.
1. What a Puppeteer handle represents
A JSHandle is a Node-side wrapper around an object that lives in the page. It is a reference, not a copy of the object. Puppeteer keeps the referenced page object from being garbage-collected while the handle is active. Dispose of a handle when you are finished with it. Puppeteer also auto-disposes handles when their associated frame navigates away or their parent execution context is destroyed, but explicit cleanup makes ownership clear.
ElementHandle is the element-specific form of a handle. It extends JSHandle and provides operations for interacting with a DOM element, such as clicking it. Other JavaScript objects returned by evaluateHandle() are represented by JSHandle.
| Need | Use | Result |
|---|---|---|
| Read text, numbers, arrays, or plain data | page.evaluate() |
A value serialized into Node.js |
| Keep a reference to a page-side object | page.evaluateHandle() |
A JSHandle or ElementHandle |
| Operate on a known DOM element | A selector API or an ElementHandle |
An element reference with DOM operations |
2. Get a handle and use it
This runnable example navigates to a page, obtains a handle to the document body, reads its HTML through the handle, and disposes the handle even if the read fails.
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
const bodyHandle = await page.evaluateHandle(() => document.body);
try {
const html = await bodyHandle.evaluate(body => body.innerHTML);
console.log(html);
} finally {
await bodyHandle.dispose();
}
} finally {
await browser.close();
}
Install the dependencies with npm install puppeteer and save the example as an .mjs file, or use "type": "module" in your package configuration. evaluateHandle() waits for a returned promise to resolve. Its page function runs in the browser’s page context.
3. Choose between evaluate and evaluateHandle
Both methods run a function in the page. The difference is how the result comes back:
page.evaluate(fn, ...args)returns the function’s result through serialization. Choose this for data you want to use in Node.js.page.evaluateHandle(fn, ...args)returns a handle to the page-side result. Choose this when you need to pass the object to another page evaluation or use an element-specific operation.
A DOM node does not serialize into a useful Node-side DOM object. Returning one through evaluate() can give you an empty object. Return a handle if you need the node itself:
const buttonHandle = await page.evaluateHandle(() =>
document.querySelector('button')
);
const button = buttonHandle.asElement();
if (button) {
await button.click();
}
await buttonHandle.dispose();
asElement() returns the same handle as an ElementHandle when the referenced object is a DOM element; otherwise it returns null. A selector may match no element, in which case the example’s check safely skips the click.
For a value-only task, avoid creating a handle unnecessarily:
const pageTitle = await page.evaluate(() => document.title);
console.log(pageTitle);
4. Pass values and handles into page code
The function you pass to an evaluation method is converted to a string and executed in the page. It cannot read variables from the surrounding Node.js lexical scope. Pass ordinary values as arguments instead:
const selector = 'main h1';
const headingText = await page.evaluate(
selector => document.querySelector(selector)?.textContent?.trim() ?? null,
selector
);
console.log(headingText);
Handles can also be passed as arguments. Puppeteer resolves the handle to its page-side object for the evaluation:
const bodyHandle = await page.evaluateHandle(() => document.body);
try {
const resultHandle = await page.evaluateHandle(
body => body.innerHTML,
bodyHandle
);
try {
console.log(await resultHandle.jsonValue());
} finally {
await resultHandle.dispose();
}
} finally {
await bodyHandle.dispose();
}
Use this pattern when subsequent operations need the page object. If the final result is only a string, number, or other serializable value, a direct evaluate() call is usually simpler.
5. Read a handle’s value or properties
Get serializable data with jsonValue()
handle.jsonValue() returns the serializable portions of the referenced object as a Node-side value. It does not call the object’s toJSON() method, and serialization can throw when the object is circular. Use it when you need data rather than a live page-side reference.
const dataHandle = await page.evaluateHandle(() => ({
title: document.title,
linkCount: document.querySelectorAll('a').length
}));
try {
const data = await dataHandle.jsonValue();
console.log(data);
} finally {
await dataHandle.dispose();
}
Get a single property
getProperty(name) returns a handle for that property. Dispose it when finished:
const documentHandle = await page.evaluateHandle(() => document);
let titleHandle;
try {
titleHandle = await documentHandle.getProperty('title');
console.log(await titleHandle.jsonValue());
} finally {
if (titleHandle) await titleHandle.dispose();
await documentHandle.dispose();
}
Inspect multiple properties
getProperties() returns a map of property names to handles. The property handles have their own lifecycle. Dispose each one you retain, then dispose the parent:
const objectHandle = await page.evaluateHandle(() => ({
heading: document.querySelector('h1')?.textContent ?? null,
url: location.href
}));
let properties;
try {
properties = await objectHandle.getProperties();
for (const [name, propertyHandle] of properties) {
try {
console.log(name, await propertyHandle.jsonValue());
} finally {
await propertyHandle.dispose();
}
}
} finally {
await objectHandle.dispose();
}
For structured data that you intend to consume as a whole, returning it from evaluate() can be more direct than enumerating property handles.
6. Release handles safely
Call dispose() after the last operation that needs the handle. Use try/finally when intervening code can throw, as in the examples above. Handles do not need to survive longer than the page-side work that uses them.
- Dispose handles created by
evaluateHandle(),getProperty(), andgetProperties()when you are done. - If a property map contains handles, account for those handles too; disposing only the parent does not express ownership of the individual references in your Node code.
- Navigation or destruction of the parent context auto-disposes associated handles. Treat that as lifecycle behavior, not as your regular cleanup plan.
- Avoid retaining handles across navigations. After a frame changes or the context is destroyed, a handle may no longer be usable.
See the official JSHandle API, Page.evaluateHandle API, dispose API, and JavaScript execution guide.
7. Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
A DOM node becomes {} or unusable data |
The node was returned through value serialization. | Use evaluateHandle() when you need the node reference, or extract the specific text or attributes with evaluate(). |
| A variable inside the page function is undefined | The function runs in the page context and cannot close over Node.js scope. | Pass the value as an argument to evaluate() or evaluateHandle(). |
asElement() returns null |
The handle points to a non-element object, or the query returned null. |
Check the returned value and only call element methods after confirming an element handle exists. |
| An operation fails after navigation | The handle belonged to the previous frame context, which was destroyed. | Create a fresh handle after navigation; do not reuse the old one. |
jsonValue() throws during serialization |
The referenced value contains a cycle or otherwise cannot be serialized. | Evaluate only the fields needed into a plain serializable object, or keep using the handle in page context. |
| Memory use grows during repeated work | Handles, including property handles, are retained without disposal. | Dispose each handle in a finally block when finished; avoid storing handles longer than needed. |
TypeScript treats a result as a generic JSHandle |
The inferred return type may not reflect that the page function returns a DOM element. | Narrow with asElement() and check for null; consult the installed release’s typings before using a generic overload. |
8. Performance, reliability, and cost
Handles are useful when several page-side operations need the same object: keeping the reference avoids converting that object into a Node-side value and then trying to recreate it. For one small piece of data, a single evaluate() call is generally easier to read. Avoid many tiny round trips when a single page evaluation can collect the required values.
Every handle has a lifetime in the page context. Releasing unused handles helps avoid retaining page objects longer than necessary, especially in loops or long-lived browser sessions. Navigation destroys the old context, so acquire handles after the relevant navigation and dispose them before moving on when practical.
Puppeteer handles have no separate per-handle charge described in the API. Operational cost comes from running your browser process and the infrastructure around it; this dossier provides no benchmark or pricing figures for Puppeteer. For a one-off screenshot rather than custom page-side automation, a screenshot API can avoid browser setup, though it does not replace Puppeteer when you need application-specific JavaScript interaction.
9. FAQ
What is a JSHandle in Puppeteer?
It is a reference wrapper for a JavaScript object that remains in the browser page context.
How do I get an ElementHandle?
Return a DOM element from page.evaluateHandle(), then use the returned handle’s element methods or narrow it with asElement().
Do I have to dispose every handle?
Dispose handles when you are finished with them. Puppeteer also clears them when their frame navigates or parent context is destroyed, but explicit cleanup is the reliable routine.
Can I return a handle from page.evaluate()?
evaluate() returns serialized values. Use evaluateHandle() to retain an in-page reference.
Or skip the browser setup
If your goal is a screenshot rather than custom Puppeteer interaction, ScreenshotNeo provides a one-request website screenshot API. See the ScreenshotNeo documentation for request options.
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; supported consent banners, newsletter popups, and chat widgets are removed, and each step can be turned off.
- Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; response headers report the page verdict and billing status.
- An MCP server provides
take_screenshot,get_page_info, andcapture_pdffor AI agents and MCP clients. - 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 for 1,000 screenshots a month with no card.


