How to Get a JavaScript Handle from a Puppeteer Frame
Use Frame.evaluateHandle() to keep a reference to an object in a Puppeteer frame. Learn frame selection, safe handle use, cleanup, and common fixes.
Call evaluateHandle() on the Puppeteer Frame whose JavaScript context contains the object: const handle = await frame.evaluateHandle(() => window.someObject). Use frame.evaluate() when you only need a serializable value in Node.js. Use evaluateHandle() when you need to retain a reference to an in-page object such as a DOM node. See Puppeteer’s Frame.evaluateHandle API and Frame reference.
Choose the frame that owns the object
A page can contain a main frame, child frames, and nested child frames. Each frame has its own JavaScript context. Calling page.evaluateHandle() runs in the main frame; for an object in an iframe, call evaluateHandle() on that iframe’s Frame.
const mainFrame = page.mainFrame();
const childFrames = mainFrame.childFrames();
Choose the target by a stable property, such as a known frame URL or its position in a frame relationship you have verified. A URL substring is convenient for an example, but production code should use a predicate that uniquely identifies the intended frame on the pages you handle.
Complete runnable example
This Node.js example launches Chromium, opens a page, locates a child frame by URL, obtains an object handle from that frame, reads a property through the handle, and disposes it. Install Puppeteer with npm install puppeteer and save this as frame-handle.js.
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
// Replace this with a stable criterion for the iframe in your page.
const frame = page.frames().find(candidate =>
candidate.url().includes('/embedded/')
);
if (!frame) {
throw new Error('Target frame not found');
}
const handle = await frame.evaluateHandle(() => window.someObject);
try {
const name = await handle.evaluate(object => object.name);
console.log(name);
} finally {
await handle.dispose();
}
} finally {
await browser.close();
}
})().catch(error => {
console.error(error);
process.exitCode = 1;
});
The URL and window.someObject are placeholders: use a real frame criterion and an object that exists in that frame. This example uses Puppeteer’s CommonJS package import. Check the API reference matching the Puppeteer version installed in your project if its types or signature differ.
Get a handle to a document or DOM element
The callback runs inside the selected frame, so browser globals such as document are available there.
const documentHandle = await frame.evaluateHandle(() => document);
try {
const title = await documentHandle.evaluate(doc => doc.title);
console.log(title);
} finally {
await documentHandle.dispose();
}
const elementHandle = await frame.evaluateHandle(() =>
document.querySelector('button')
);
try {
if (await elementHandle.evaluate(element => element === null)) {
throw new Error('Button not found');
}
const label = await elementHandle.evaluate(element => element.textContent);
console.log(label);
} finally {
await elementHandle.dispose();
}
A returned DOM element is represented as an ElementHandle; other returned objects are generally JSHandle instances. If the selector is the whole task, frame selector methods are usually more direct:
const button = await frame.$('button');
if (!button) throw new Error('Button not found');
try {
console.log(await button.evaluate(element => element.textContent));
} finally {
await button.dispose();
}
const labels = await frame.$$eval('button', buttons =>
buttons.map(button => button.textContent)
);
Pass values into the frame callback
The callback is serialized and evaluated in the browser context. It cannot access local Node.js variables or helper functions through a closure. Pass values as arguments instead.
const propertyName = 'name';
const handle = await frame.evaluateHandle(name => window[name], propertyName);
try {
console.log(await handle.evaluate(value => String(value)));
} finally {
await handle.dispose();
}
Pass data, not Node.js objects that depend on local prototypes, functions, or process state. Values crossing into the page context must be supported by Puppeteer’s argument handling. If you only need a plain serializable result, return it from evaluate() instead.
Handle lifecycle and frame navigation
A handle keeps its referenced in-page object from being garbage-collected. Dispose of it when finished, preferably with try/finally so errors do not skip cleanup. Puppeteer also disposes handles when their frame navigates away or the parent execution context is destroyed, but code should not rely on an old handle surviving those events.
- Acquire and use the handle while the target frame remains in the same relevant document lifecycle.
- After navigation or context destruction, locate the current frame and evaluate again to obtain a fresh handle.
- Do not retain many handles in long-running crawlers; dispose each one promptly.
Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| Frame not found | The iframe has not attached yet, the predicate matches no frame, or the URL changed. | Wait for the page’s iframe condition, inspect page.frames() and frame.url(), and use a unique criterion. |
| Value is undefined or null | The property does not exist yet, the selector did not match, or the code ran in the wrong frame. | Confirm the target frame and wait for the relevant page state before evaluating; check the result explicitly. |
| Callback says a Node variable is undefined | The function runs in the browser and does not close over Node.js scope. | Pass the variable after the callback as an argument. |
| Handle is disposed or execution context was destroyed | The frame navigated, detached, or its document context was replaced. | Reacquire the frame and handle after navigation; do not reuse the stale handle. |
| Returned object is hard to inspect or serialize | A DOM node or complex object is a remote reference, not a plain JSON value. | Use the handle with evaluate() to extract the specific serializable fields you need. |
| Memory or protocol resource usage grows | Handles are retained without disposal. | Dispose handles in finally blocks and avoid accumulating remote references. |
Performance, reliability, and cost
evaluateHandle() is useful when subsequent operations need the same remote object. If you need only a string, number, boolean, or structured data, evaluate() avoids keeping a remote reference alive. Prefer selector helpers for straightforward element selection and extraction. Keep evaluation callbacks small, wait for the needed frame state rather than repeatedly polling with expensive work, and reacquire handles when navigation changes the execution context.
Puppeteer itself is software you run with a browser; this API call does not have a per-call ScreenshotNeo price. Your operational costs depend on your own browser runtime and infrastructure. ScreenshotNeo is a separate website screenshot API with a free allowance and paid plans described below.
Or skip the browser setup
If your goal is a screenshot rather than a live JavaScript object reference, ScreenshotNeo can return a screenshot or PDF with one request. This does not replace Puppeteer handles when you need to inspect or manipulate an in-page object.
ScreenshotNeo API documentation
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}`);
ScreenshotNeo accepts cookie and consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers report the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The free plan includes 1,000 shots a month with no card; paid plans start at $5 for 3,000 shots. Sign up for 1,000 free screenshots a month, with no card.
FAQ
Can I use a handle from an iframe with page.evaluate()?
Use the handle’s supported methods or pass it as an argument where appropriate. To run code in the iframe context, call frame.evaluate() on that frame.
Should I use evaluateHandle() for every value?
No. Use evaluate() for values you can serialize and return to Node.js. Keep a handle when you need a reference to an in-page object.
Does a child frame share the main frame’s JavaScript globals?
No. Each frame has its own execution context. Evaluate in the frame that owns the document or object you need.
What if the frame reloads while I am using its handle?
The old context and handle can be invalidated. Wait for the new frame state, then get a fresh handle.


