How to Inspect a Puppeteer Remote Object
Inspect page-side JavaScript values with Puppeteer’s JSHandle methods, choose between a live handle, serializable data, and CDP metadata, and clean up safely.
A Puppeteer remote object is a JavaScript value that lives in the browser page’s execution context. To inspect it without first copying it into Node.js, get a JSHandle with page.evaluateHandle(), then use handle.evaluate(), getProperty(), or getProperties(). Use jsonValue() when you need serializable data, remoteObject() when you need Chrome DevTools Protocol metadata, and dispose() when you are finished.
A JSHandle is a reference to a page-side value, not a plain JavaScript object copied into your Node.js process. This distinction matters for large objects, DOM nodes, values that JSON cannot represent, and any inspection that requires the page’s live state. See the official Puppeteer JSHandle API and the Chrome DevTools Protocol Runtime reference.
1. Choose the right way to inspect the value
| What you need | Use | What you get |
|---|---|---|
| Keep a page-side object available for more inspection | page.evaluateHandle() |
A JSHandle referring to the object in the page context. |
| Read a few fields or derive a compact summary | handle.evaluate(fn) |
The function’s returned value, evaluated with the referenced value as its first argument. |
| Get one property as a separately managed handle | handle.getProperty(name) |
A JSHandle for that property. |
| Enumerate properties as handles | handle.getProperties() |
A map from property names to handles. |
| Get a by-value representation | handle.jsonValue() |
The serializable portions of the value; it does not invoke toJSON(). |
| Inspect protocol metadata | handle.remoteObject() |
The underlying Protocol.Runtime.RemoteObject representation. |
| Release the reference | handle.dispose() |
Releases the handle so its referenced object can be garbage-collected when otherwise unreachable. |
If you only need a value returned to Node.js, page.evaluate() may be simpler. Use a handle when you need to keep working with a value that remains in the page context.
2. Create a remote object handle
This runnable example starts Chromium through Puppeteer, creates a page-side object, reads selected values through a handle, inspects properties, and disposes every handle it creates. Install Puppeteer first with npm install puppeteer, save this as inspect.js, and run node inspect.js.
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({ headless: true });
let stateHandle;
let userHandle;
let properties;
try {
const page = await browser.newPage();
await page.setContent('<title>Handle example</title>');
await page.evaluate(() => {
window.appState = {
status: 'ready',
user: { id: 42, name: 'Ada' },
updatedAt: new Date('2026-01-01T00:00:00Z'),
};
});
// Keep a reference to the object in the page context.
stateHandle = await page.evaluateHandle(() => window.appState);
// Read a focused summary. The callback runs in the page context.
const summary = await stateHandle.evaluate(state => ({
status: state.status,
userName: state.user.name,
updatedAt: state.updatedAt.toISOString(),
}));
console.log('Summary:', summary);
// Obtain one property as its own handle, then convert that value.
userHandle = await stateHandle.getProperty('user');
console.log('User:', await userHandle.jsonValue());
// Enumerate property names and inspect the handles you care about.
properties = await stateHandle.getProperties();
console.log('Property names:', [...properties.keys()]);
console.log('Status:', await properties.get('status').jsonValue());
// This is protocol metadata, not a recursive copy of the live object.
console.log('Remote object metadata:', stateHandle.remoteObject());
} finally {
if (properties) {
await Promise.all([...properties.values()].map(property => property.dispose()));
}
if (userHandle) await userHandle.dispose();
if (stateHandle) await stateHandle.dispose();
await browser.close();
}
})();
The example reads only the fields needed for its summary. That is often more useful than attempting to convert a large or complex object wholesale.
3. Inspect a value with each JSHandle method
Read or derive values with handle.evaluate()
handle.evaluate(fn) runs fn in the page context and passes the referenced value as its first argument. Return the smallest useful result to Node.js:
const result = await handle.evaluate(value => ({
type: typeof value,
keys: Object.keys(value),
ready: value.status === 'ready',
}));
For arrays and nested objects, map to the fields you need rather than returning an unbounded structure. The function runs against the page’s value; it does not turn the handle itself into a local object.
Inspect one property with getProperty()
const nameHandle = await handle.getProperty('name');
try {
console.log(await nameHandle.jsonValue());
} finally {
await nameHandle.dispose();
}
The result is another handle, so it has its own lifetime. Dispose it when finished.
Enumerate property handles with getProperties()
const propertyHandles = await handle.getProperties();
try {
for (const [name, propertyHandle] of propertyHandles) {
console.log(name, await propertyHandle.jsonValue());
}
} finally {
await Promise.all([...propertyHandles.values()].map(item => item.dispose()));
}
Property enumeration returns handles, not a plain object. Some values may not have useful JSON representations, and recursively enumerating a large object can be expensive. Select specific properties where possible.
Convert serializable data with jsonValue()
const plainValue = await handle.jsonValue();
console.log(plainValue);
This is suitable when a by-value representation is what you need. Puppeteer documents that jsonValue() returns serializable portions and does not call the value’s toJSON() method. It is not a guarantee that every detail of every live runtime value can be represented. For a focused representation, use evaluate() to select fields deliberately.
Read CDP metadata with remoteObject()
const remote = handle.remoteObject();
console.log({
type: remote.type,
subtype: remote.subtype,
className: remote.className,
description: remote.description,
objectId: remote.objectId,
value: remote.value,
unserializableValue: remote.unserializableValue,
});
The fields present depend on the value and protocol representation. CDP’s Runtime.RemoteObject can describe type, subtype, class name, description, object identifier, serialized value, and certain values JSON cannot represent. An objectId identifies a remote object for protocol operations; it is not the object copied into Node.js.
4. When to use CDP directly
For ordinary application debugging, JSHandle methods are usually the more direct route. Use CDP when you specifically need protocol-level property descriptors or need to issue Runtime commands yourself. Puppeteer’s Page.createCDPSession() creates a DevTools Protocol session attached to the page. The Runtime domain’s property inspection operation works from a remote object identifier and returns property descriptors; consult the protocol reference for the exact command and fields supported by your Chrome version.
const client = await page.createCDPSession();
let objectId;
try {
const { result } = await client.send('Runtime.evaluate', {
expression: 'window.appState',
objectGroup: 'inspection',
returnByValue: false,
});
objectId = result.objectId;
console.log('Remote descriptor:', result);
if (objectId) {
const { result: descriptors } = await client.send(
'Runtime.getProperties',
{ objectId, ownProperties: true }
);
console.log(descriptors);
}
} finally {
// Release objects retained in this protocol object group.
await client.send('Runtime.releaseObjectGroup', { objectGroup: 'inspection' });
await client.detach();
}
This lower-level example is intended for a page where window.appState has already been set. CDP command details can vary by protocol version, so check the version reference matching your browser. If you already have a Puppeteer handle, handle.remoteObject() exposes its protocol representation without creating a second evaluation path.
5. Understand serialization limits and special values
- Live reference versus copied value: a handle refers to an object in the page context. It is not a plain object in Node.js.
- Non-serializable values: JSON cannot represent every JavaScript runtime value. CDP’s remote representation includes an
unserializableValuefield for certain primitives that JSON cannot express. - Partial representations:
jsonValue()provides serializable portions and does not runtoJSON(). Choose fields throughevaluate()if you need a predictable summary. - Protocol metadata is not a deep dump:
remoteObject()describes the remote value; it does not recursively copy an arbitrary live object into Node.js. - Nested handles have separate lifetimes: dispose property handles as well as the original handle.
6. Dispose handles and handle navigation
Dispose handles after inspection so Puppeteer can release their references. A property handle created with getProperty() or returned by getProperties() should also be disposed. Use try/finally to make cleanup happen even if inspection throws.
const handle = await page.evaluateHandle(() => window.appState);
try {
console.log(await handle.evaluate(value => value.status));
} finally {
await handle.dispose();
}
Puppeteer documents that handles are automatically disposed when their associated frame navigates away or their parent execution context is destroyed. After navigation or context destruction, do not assume an old handle remains usable; acquire a new handle in the current page context.
7. Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| The logged handle does not show the object’s fields. | A JSHandle is a remote reference, not a local copy. |
Use handle.evaluate() for selected fields or jsonValue() for serializable data. |
jsonValue() omits details or returns an unexpected representation. |
The value contains runtime details that are not serializable; toJSON() is not called. |
Select the needed fields with evaluate(), or inspect remoteObject() for protocol metadata. |
| A handle operation fails after navigation. | The frame navigated and its execution context was destroyed; the handle was automatically disposed. | Wait for navigation to finish, then create a fresh handle from the current page context. |
| A property handle remains around after the main handle is disposed. | Property handles have their own lifetimes. | Dispose every handle returned by getProperty() and getProperties(), preferably in finally. |
| CDP property inspection cannot use the returned object. | The evaluated result may not be an object with an objectId, or the execution context may have changed. |
Check the returned RemoteObject fields, ensure the result is a live remote object, and evaluate again in the current context. |
| Inspection is slow or produces too much output. | The code is traversing or serializing a large object graph. | Read only required fields with evaluate(); avoid recursively enumerating every property. |
8. Performance, reliability, and cost
Handle operations keep the value in the browser context and send back only the result of each inspection operation. For large objects, selecting a compact summary with evaluate() can reduce unnecessary serialization and output. Enumerating every property or repeatedly making round trips adds work; group related reads into one evaluation when practical.
Handles are tied to their page execution context. Navigation and context destruction end that lifetime, so make inspection occur after the relevant page state is ready and reacquire handles after navigation. Explicit disposal keeps long-running automation from accumulating unnecessary references.
Puppeteer itself is a software library; this API inspection workflow has no per-handle charge specified by the cited documentation. If you need rendered screenshots as part of debugging or automation, ScreenshotNeo is a website screenshot API and MCP server. Its pricing is listed below in the ScreenshotNeo section.
9. ScreenshotNeo: capture the rendered page through an API
JSHandle inspection is for values inside a page’s JavaScript context. If the task is to capture the rendered page as an image or PDF, ScreenshotNeo can do that with one GET request. It is also an MCP server for AI agents, including Claude, Cursor, and other MCP clients. See the ScreenshotNeo API documentation for request options.
cURL
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}`);
ScreenshotNeo removes cookie and consent banners from more than 60 known 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 provides take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots per month without a card; paid plans start at $5 for 3,000 screenshots. Sign up for 1,000 free screenshots a month, with no card required.
10. Frequently asked questions
How do I inspect a Puppeteer JSHandle?
Call evaluateHandle() to get the handle, then inspect it with evaluate(), getProperty(), getProperties(), or remoteObject(), depending on what you need.
Does jsonValue() call the object’s toJSON() method?
No. Puppeteer documents that jsonValue() does not call toJSON().
What does remoteObject() return?
It returns the protocol Runtime.RemoteObject representation behind the handle: metadata about the remote value, rather than a complete local copy.
When should I use CDP instead of JSHandle methods?
Use CDP directly when you need protocol operations such as property descriptors. For focused application-level inspection, JSHandle methods generally provide the simpler interface.


