How to Get Object Properties with Puppeteer
Read page object values with Puppeteer’s evaluate APIs, or keep a live reference with JSHandle. See runnable patterns, cleanup, and common fixes.
Use page.evaluate() when you need property values in Node.js, and return a serializable object, array, or primitive. Use page.evaluateHandle() when you need to retain a reference to the in-page object; then call getProperty() for one property or getProperties() to retrieve the properties Puppeteer represents as handles.
The distinction matters: evaluate() returns serialized data, while handle APIs refer to objects that remain in the browser context. A DOM node returned through ordinary evaluation is not a usable Node-side DOM object.
Choose the right API
| Need | Use | What Node.js receives |
|---|---|---|
| A few data values | page.evaluate() |
A serialized and reconstructed value |
| A reference to the original page object | page.evaluateHandle() |
A JSHandle, or an ElementHandle for an element |
| One property from an existing handle | handle.getProperty(name) |
A handle to that property |
| Properties represented by an existing handle | handle.getProperties() |
A Map<string, JSHandle> |
| Heap objects matching a prototype | page.queryObjects(prototypeHandle) |
A handle to an array; specialized heap inspection |
For ordinary extraction, start with evaluate(). It avoids keeping remote handles alive and makes it clear which data crosses from the page to Node.
Read selected values with page.evaluate()
Suppose the page exposes window.product. Read and shape its values inside the callback:
const product = await page.evaluate(() => {
const item = window.product;
return {
name: item.name,
price: item.price,
available: item.available,
};
});
console.log(product);
The callback runs in the page context. Its result is serialized and reconstructed in Node.js, so return data you can serialize rather than expecting a live reference to the original object.
Pass Node-side values as arguments
The callback is serialized and sent to the page. It cannot close over Node-side variables or invoke Node-side helper functions. Pass inputs explicitly:
const propertyName = 'title';
const value = await page.evaluate((name) => {
return window.article?.[name] ?? null;
}, propertyName);
Define any helper logic the page callback needs inside the callback, or pass plain input values as arguments. A returned promise is awaited by Puppeteer.
Return a safe snapshot
Select the fields needed by the caller, and account for properties that may not exist or may have changed type:
const snapshot = await page.evaluate(() => {
const item = window.product;
if (!item || typeof item !== 'object') {
return { found: false };
}
return {
found: true,
name: typeof item.name === 'string' ? item.name : null,
count: Number.isFinite(item.count) ? item.count : null,
};
});
This is usually more robust than trying to transfer an entire application object. It also makes the output shape explicit for downstream code.
Get one property from a JSHandle
Use a handle when you need the page-side object reference, or when the property itself must remain represented by a handle. The official getProperty() API fetches a single property from the referenced object.
const objectHandle = await page.evaluateHandle(() => window.product);
const nameHandle = await objectHandle.getProperty('name');
try {
const name = await nameHandle.jsonValue();
console.log(name);
} finally {
await nameHandle.dispose();
await objectHandle.dispose();
}
getProperty() returns a handle, not the ordinary value. Call jsonValue() when the property value is serializable and you need it in Node. Dispose both handles when finished.
Enumerate properties with getProperties()
getProperties() returns a map of handles representing properties of the current handle. It is useful when the property names are not known in advance. The returned map is not a promise that every JavaScript property category or inherited property is exposed; use the API documentation for your installed Puppeteer version when exact reflection behavior matters.
const objectHandle = await page.evaluateHandle(() => window.product);
const properties = await objectHandle.getProperties();
try {
const result = {};
for (const [name, propertyHandle] of properties) {
try {
result[name] = await propertyHandle.jsonValue();
} catch {
// Some property values may not be serializable as ordinary data.
result[name] = '[value unavailable as JSON]';
}
}
console.log(result);
} finally {
for (const propertyHandle of properties.values()) {
await propertyHandle.dispose();
}
await objectHandle.dispose();
}
For property values that are objects, jsonValue() may not give you the recursive live object graph you want. Decide which nested values to read, then evaluate or obtain handles for those values explicitly. For the documented API shape, see JSHandle.getProperties().
Work with DOM elements
To read ordinary DOM data, query and extract it inside evaluate():
const heading = await page.evaluate(() => {
const element = document.querySelector('h1');
return element ? {
text: element.textContent?.trim() ?? '',
id: element.id,
tagName: element.tagName,
} : null;
});
To keep an element reference on the Node side, use handle-based evaluation. Puppeteer represents an element result as an ElementHandle:
const headingHandle = await page.evaluateHandle(() => document.querySelector('h1'));
try {
const text = await headingHandle.evaluate(element => element.textContent?.trim() ?? '');
console.log(text);
} finally {
await headingHandle.dispose();
}
Do not return a DOM node from ordinary evaluate() and expect it to function as a browser element in Node. Return the needed fields, or use an element handle for page-side operations.
Complete runnable example
This example launches Chromium, creates a page with a known object, reads a serializable snapshot, and then demonstrates a handle property read. Install Puppeteer with npm install puppeteer, save the code as properties.js, and run node properties.js.
const puppeteer = require('puppeteer');
async function main() {
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
await page.setContent(`
<script>
window.product = { name: 'Notebook', price: 12, available: true };
</script>
`);
// Prefer evaluate() for values that should cross into Node.js.
const snapshot = await page.evaluate(() => {
const item = window.product;
return {
name: item.name,
price: item.price,
available: item.available,
};
});
console.log('Snapshot:', snapshot);
// Use handles when a page-side object reference is needed.
const objectHandle = await page.evaluateHandle(() => window.product);
const priceHandle = await objectHandle.getProperty('price');
try {
console.log('Price via handle:', await priceHandle.jsonValue());
} finally {
await priceHandle.dispose();
await objectHandle.dispose();
}
} finally {
await browser.close();
}
}
main().catch(error => {
console.error(error);
process.exitCode = 1;
});
Or skip the browser setup
If the goal is a visual capture rather than inspecting JavaScript values, ScreenshotNeo provides a website screenshot API and MCP server. It does not expose arbitrary page object properties; use Puppeteer for that. For screenshots, one GET request returns an image or PDF, and the full options are in the ScreenshotNeo documentation.
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
Python:
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)
Node.js:
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}`);
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));
ScreenshotNeo accepts cookie or consent banners as a visitor 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 and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and billing status. Its MCP server offers take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots.
Sign up for ScreenshotNeo and get 1,000 free screenshots a month, with no card required.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
The result is undefined or null |
The property is absent, the page object has not been initialized, or the wrong page/frame is being queried. | Wait for the relevant page state, verify the object inside the page context, and check the frame containing it. Use optional checks when a property may be missing. |
ReferenceError for a Node variable inside the callback |
The callback runs in the page and cannot close over Node lexical scope. | Pass the value as an evaluate argument or define the helper in the callback. |
| A DOM result looks like an empty object | Ordinary evaluation serializes the return value; it does not create a live Node-side DOM object. | Return text or attributes from inside evaluate(), or use evaluateHandle() for an element handle. |
jsonValue() fails or loses useful detail |
The value may not be representable as ordinary serialized data, or it may be a browser object. | Read a serializable subset inside evaluate(); keep a handle and operate on the value in page context if reference semantics are required. |
| Handle operations fail after navigation | The page frame or execution context was destroyed. Handles are automatically disposed when their frame navigates away or their parent context is destroyed. | Wait for navigation to finish, then reacquire the object handle in the new page context. |
| Memory or remote object usage grows during a long run | Handles remain references until disposed or their context is destroyed. | Dispose property handles and parent handles in finally blocks as soon as they are no longer needed. |
A property is missing from getProperties() |
The returned map represents properties according to the API behavior; do not assume it is a complete reflection of all property types or inheritance. | Read a known property by name with getProperty(), or inspect the relevant values inside evaluate(). |
Performance, reliability, and cost
- Minimize boundary transfers: return only fields the caller needs from
evaluate(), rather than copying a large object graph. - Prefer snapshots for extraction: fewer retained handles mean less lifecycle bookkeeping and fewer stale references after navigation.
- Use handles when they earn their cost: they are useful for page-side object or element operations, but dispose them promptly.
- Wait for the right state: an object may be populated asynchronously. Wait for an application-specific selector or condition before reading it; avoid arbitrary sleeps when a state condition is available.
- Expect context changes: navigation and frame teardown invalidate the context holding an object. Reacquire handles after such transitions.
- API cost: Puppeteer property access itself has no ScreenshotNeo charge. If you choose ScreenshotNeo for screenshot capture, its free and paid tiers are listed above; it is not a replacement for reading arbitrary in-page JavaScript properties.
FAQ
Can I get a property when I only know its name at runtime?
Yes. Pass the property name into evaluate() and access it with bracket notation, or pass it to getProperty(name) on a handle.
Does getProperties() include inherited properties?
Do not assume that it is a full JavaScript reflection API. The documented result is a map of represented property handles. For a specific inherited value, read it by name in page context.
When should I use queryObjects()?
Use it for specialized heap inspection when you have a prototype handle and want objects associated with that prototype. For a known page object, evaluate() or a handle property method is simpler.
Do I need to dispose a handle if the page is closing?
Handles are automatically disposed when their frame navigates away or parent context is destroyed. Explicit cleanup is still a clear and reliable practice, especially in loops or long-lived browser sessions.


