How to Run JavaScript in a Puppeteer Frame
Run JavaScript in the right Puppeteer frame with frame.evaluate(). Learn how to find frames, pass values, wait for content, return results, and troubleshoot common errors.
To run JavaScript inside an iframe with Puppeteer, select its Frame object and call await frame.evaluate(pageFunction, ...args). The callback runs in that frame’s browser context. Puppeteer passes any trailing arguments into it, waits for a returned promise, and gives Node.js the result.
const frame = page.frames().find(candidate => candidate.url().includes('/widget'));
if (!frame) throw new Error('Target frame was not found');
const title = await frame.evaluate(() => document.title);
console.log(title);
page.evaluate() runs in the main frame. Use frame.evaluate() for a child frame. The same pattern works for the main frame if you obtain it with page.mainFrame(). See Puppeteer’s Frame.evaluate() reference and JavaScript execution guide.
1. Install Puppeteer and start a page
The following complete example starts Chromium, opens a page, locates a frame by URL, waits for content in that frame, reads it, and closes the browser even if an operation fails. Install Puppeteer in your project with npm install puppeteer.
const puppeteer = require('puppeteer');
async function main() {
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
const frame = page.frames().find(candidate => candidate.url().includes('/widget'));
if (!frame) throw new Error('Widget frame was not found');
await frame.waitForSelector('.status', { timeout: 10_000 });
const status = await frame.evaluate(() =>
document.querySelector('.status')?.textContent?.trim() ?? null
);
console.log({ frameUrl: frame.url(), status });
} finally {
await browser.close();
}
}
main().catch(error => {
console.error(error);
process.exitCode = 1;
});
Replace the example URL, frame URL fragment, and selector with values for your page. A frame can navigate after the parent page loads, so select it when it is available and wait for the target content inside that frame.
2. Find the frame you want to evaluate
Puppeteer exposes the current frame tree through page.mainFrame() and page.frames(). Each frame also exposes childFrames() and parentFrame(). Frames can attach, navigate, and detach while a page is running.
const mainFrame = page.mainFrame();
console.log('Main frame:', mainFrame.url());
for (const frame of page.frames()) {
console.log({ url: frame.url(), isMainFrame: frame === mainFrame });
}
Choose a stable identifier when possible, such as a URL path or an attribute on the iframe element. Avoid relying on a frame’s array position: frame order can change as the page changes.
Identify a frame from its iframe element
If the URL does not distinguish the target, inspect each frame’s associated iframe element. The current Frame reference provides frame.frameElement(). Read the element’s name or id; Puppeteer marks frame.name() deprecated and recommends inspecting the frame element instead.
let targetFrame;
for (const candidate of page.frames()) {
const element = await candidate.frameElement();
if (!element) continue; // The main frame has no iframe element.
const nameOrId = await element.evaluate(el => el.name || el.id);
if (nameOrId === 'payment-frame') {
targetFrame = candidate;
break;
}
}
if (!targetFrame) throw new Error('Payment frame was not found');
const text = await targetFrame.evaluate(() => document.body.innerText);
For nested frames, select the nested Frame itself. Evaluating in a parent frame does not automatically run code inside its child frames.
3. Pass Node.js values into the browser context
The function passed to evaluate() is serialized and evaluated in the page. It cannot close over variables or helper functions from your Node.js scope. Pass values as arguments instead:
const selector = '.status';
const status = await frame.evaluate(selector => {
return document.querySelector(selector)?.textContent?.trim() ?? null;
}, selector);
console.log(status);
Pass multiple values as separate arguments. Keep arguments and returned values to data that can be transferred between Node.js and the page context, such as strings, numbers, booleans, arrays, and plain objects.
const result = await frame.evaluate((selector, prefix) => {
const element = document.querySelector(selector);
return element ? `${prefix}${element.textContent.trim()}` : null;
}, '.status', 'Status: ');
Do not expect Node.js modules, imported functions, or local variables to exist in the callback. Define browser-side logic inside the callback and pass in any data it needs.
4. Use the right evaluation or interaction method
| Method | Use it for | What comes back |
|---|---|---|
frame.evaluate(fn, ...args) |
Running browser-side JavaScript and returning data | A serialized result; returned promises are awaited |
frame.evaluateHandle(fn, ...args) |
Keeping a reference to a DOM node or other page object | A handle that you can use to interact with the browser object |
frame.$eval(selector, fn, ...args) |
Running a callback on the first matching element | The callback’s serialized result |
frame.$$eval(selector, fn, ...args) |
Running a callback on all matching elements | The callback’s serialized result |
frame.waitForSelector(selector, options) |
Waiting until a selector matches within this frame | An element handle, or null for the documented hidden case |
frame.locator(selector) |
Interactions such as clicking and filling that benefit from automatic waits | A locator interaction or result |
Use evaluate() when the task needs custom browser-side logic. For an element-level read, $eval or $$eval may be simpler. For an interaction that should wait for presence and state, a locator is generally a better fit. The Puppeteer page interactions guide describes locator behavior.
Read one element or a collection
const firstLabel = await frame.$eval('.label', element => element.textContent.trim());
const labels = await frame.$$eval('.label', elements =>
elements.map(element => element.textContent.trim())
);
$eval requires a matching element; if none exists, it fails. Wait for the selector first if the page renders it asynchronously.
Wait for dynamic content in the selected frame
await frame.waitForSelector('[data-ready="true"]', { timeout: 10_000 });
const state = await frame.evaluate(() => ({
title: document.title,
ready: document.querySelector('[data-ready="true"]') !== null,
}));
console.log(state);
waitForSelector() operates within the selected frame and works across navigations. It throws when a required element does not appear before the timeout. For interactions, a locator can handle waiting for presence and state automatically.
5. Handle asynchronous code and return values
If the page callback returns a promise, Puppeteer waits for it to resolve and returns its value to Node.js. This makes asynchronous browser APIs usable inside the callback.
const pageData = await frame.evaluate(async () => {
const response = await fetch('/api/status');
return response.json();
});
console.log(pageData);
The request above runs from the page context and follows that page’s browser security rules, including origin restrictions. Handle page-side failures in the callback when you need a specific fallback or error shape.
const result = await frame.evaluate(async () => {
try {
const response = await fetch('/api/status');
if (!response.ok) return { ok: false, status: response.status };
return { ok: true, data: await response.json() };
} catch (error) {
return { ok: false, error: String(error) };
}
});
Ordinary evaluation serializes results. A DOM node returned through evaluate() does not arrive as a live node reference that Node.js can use. Return a serializable representation instead, or use evaluateHandle() when you need the actual page object.
6. Keep handles short-lived
Use evaluateHandle() when you need a browser object by reference. Dispose of the handle when you are finished. Puppeteer also disposes handles when the associated frame navigates away or its context is destroyed.
const bodyHandle = await frame.evaluateHandle(() => document.body);
try {
const text = await bodyHandle.evaluate(body => body.innerText);
console.log(text);
} finally {
await bodyHandle.dispose();
}
For a one-time value such as text, a count, or an attribute, return that data directly with evaluate(); it avoids retaining a browser object handle.
7. Troubleshoot common frame evaluation errors
| Symptom | Likely cause | Fix |
|---|---|---|
| A Node variable is undefined in the callback | The callback runs in the browser context and cannot access Node’s lexical scope | Pass the value as a trailing argument to evaluate() |
The result is {} or does not act like a DOM node |
The result was serialized instead of returned as a live browser object | Return plain data, or use evaluateHandle() and dispose the handle |
| A selector lookup fails or times out | The selector is absent, incorrect, or has not rendered yet in that frame | Check the selector and frame, then wait with frame.waitForSelector() or use a locator for interaction |
| The code reads the wrong document | The selected frame was not the intended iframe, or the target is nested | Inspect frame.url() and the iframe element’s name/id; evaluate on the nested frame itself |
| The frame disappears during evaluation | The page detached or navigated the frame and destroyed its execution context | Wait for the target frame/content again, select the current frame, and retry only if the operation is safe to repeat |
| An asynchronous callback never returns | A page-side promise is still pending, or an operation waits for a condition that never occurs | Give page-side waits a clear timeout or termination condition; handle rejected promises in the callback |
A frame tree is live page state. For pages that add or replace iframes dynamically, do not retain a frame reference indefinitely across navigations; find the current frame when needed and confirm its identifying properties before evaluating.
8. Performance, reliability, and cost
Frame evaluation is a browser automation operation, so its runtime includes communication between Node.js and the browser plus whatever work the callback performs. Keep callbacks focused: return only the fields you need, avoid repeatedly scanning a large DOM, and batch related reads into one evaluation when doing so keeps the logic clear. Do not use a fixed sleep when you can wait for a specific selector or state.
For reliability, identify a frame by a meaningful URL or iframe attribute, wait for the specific content you need, and expect navigation or detachment to invalidate handles and execution contexts. Use explicit timeouts for waits so a missing widget fails in a bounded period. If an operation changes page state, consider whether retrying after a navigation could repeat that change.
Running Puppeteer yourself has costs tied to the environment you operate, such as browser compute and maintenance. There is no universal cost or speed figure for a frame evaluation; it depends on the browser, page, callback, and infrastructure. Keep a browser open for a sequence of related operations rather than launching one for every small read, where your application’s lifecycle permits it.
9. Or skip the browser setup
If your goal is a screenshot rather than arbitrary JavaScript execution inside an iframe, ScreenshotNeo provides a website screenshot API and MCP server. It does not replace frame.evaluate() for custom page logic, but it can return a screenshot or PDF with one GET request. See the ScreenshotNeo API 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,
)
r.raise_for_status()
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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const image = Buffer.from(await res.arrayBuffer());
require('node:fs').writeFileSync('shot.webp', image);
ScreenshotNeo accepts consent banners like 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 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.
10. Frequently asked questions
Does frame.evaluate() run in the iframe’s JavaScript world?
It runs in the selected frame’s browser context. Use the child frame object for an iframe; the main frame object targets the top-level document.
Can I use a function declared in my Node.js file inside the callback?
No. The callback is serialized for browser execution. Put the needed logic in the callback and pass data through its arguments.
When should I choose evaluateHandle()?
Choose it when the Node.js script needs a live reference to a browser object. For text, attributes, and other data, use evaluate() and return a serializable value.
Should I use evaluation to click an element?
Use a locator for ordinary interactions such as clicking or filling because it waits for element presence and state. Use evaluation when the interaction requires custom browser-side code.


