Use page.evaluate() to run JavaScript in the current Puppeteer page. Choose page.evaluateOnNewDocument() when code must run before site scripts, page.addScriptTag() when you need a real external or inline <script> element, and page.exposeFunction() when browser code must call a Node.js function. The right method depends on timing, execution context, scope, and cleanup.
Choose the injection method
| Need | API | Execution and scope | Return or cleanup behavior |
|---|---|---|---|
| Read page state, change the DOM, or run a function now | page.evaluate() |
Runs in the current document’s browser context | Returns the value (including an awaited Promise) |
| Patch globals, seed values, or install hooks before application code | page.evaluateOnNewDocument() |
Runs after a document is created but before its scripts; repeats for navigations and attached or navigated child frames | Returns a registration identifier that can later be removed |
| Load a URL or inline source as a script element | page.addScriptTag() |
Adds a <script> to the main frame (the page method is a shortcut for page.mainFrame().addScriptTag()) |
Returns an ElementHandle<HTMLScriptElement> |
| Let page JavaScript call a Node.js capability | page.exposeFunction() |
Adds a named function to window; its implementation runs in Node.js and remains installed across navigations |
Page calls receive a Promise for the Node-side result |
These methods are not interchangeable. A function passed to evaluate is serialized and executed in the browser, so Node.js lexical variables, modules, and filesystem access are not automatically available there. Pass data explicitly through the API’s argument parameters or use an exposed function for a deliberate Node bridge.
Run JavaScript in the current page with page.evaluate()
Call evaluate after navigation or after the state you need exists. Puppeteer evaluates the function in the page context and waits for a returned Promise, which makes asynchronous DOM work straightforward.
const title = await page.evaluate(() => document.title);
const result = await page.evaluate((selector) => {
const element = document.querySelector(selector);
return element ? element.textContent : null;
}, '#headline');
console.log({ title, result });
Pass data instead of closing over Node variables
This code does not work as many first attempts expect:
Recommended Free Tools
#1 Best Overall
const selector = '#headline';
// The browser function cannot see Node's selector variable by lexical scope.
await page.evaluate(() => document.querySelector(selector));
Use an argument instead:
const selector = '#headline';
const text = await page.evaluate((css) => {
return document.querySelector(css)?.textContent ?? null;
}, selector);
Return plain, serializable data such as strings, numbers, booleans, arrays, and objects. For DOM nodes or other execution-context handles, extract the properties you need inside the page function rather than trying to return the handle as ordinary JSON.
Wait for asynchronous page work
const value = await page.evaluate(async () => {
const response = await fetch('/api/status');
const data = await response.json();
return data.state;
});
If injected code can trigger navigation, coordinate the action and navigation wait together to avoid a race:
await Promise.all([
page.waitForNavigation(),
page.evaluate(() => document.querySelector('a.next')?.click()),
]);
Use a navigation-specific wait condition appropriate to your application when the default load event is not sufficient.
Inject before the site’s scripts with page.evaluateOnNewDocument()
evaluateOnNewDocument is Puppeteer’s preload mechanism. The function is invoked after the document is created but before any of that document’s scripts run. Register it before the navigation whose code you need to precede.
The Tool Desk
Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Rank #2
- HTML CSS Design and Build Web Sites
- Comes with secure packaging
- It can be a gift option
await page.evaluateOnNewDocument((value) => {
Object.defineProperty(window, '__BUILD_LABEL__', {
configurable: false,
value,
});
}, 'test-build');
await page.goto('https://example.com');
const label = await page.evaluate(() => window.__BUILD_LABEL__);
console.log(label);
Load a larger preload file
const fs = require('node:fs');
const preload = fs.readFileSync('./preload.js', 'utf8');
const registration = await page.evaluateOnNewDocument(preload);
await page.goto(targetUrl);
// When the instrumentation scope ends:
await page.removeScriptToEvaluateOnNewDocument(registration.identifier);
The registration is persistent across future navigations until removed. Keep its identifier and remove it when a test, recording session, or instrumentation task is over. The hook is also invoked when child frames are attached or navigated. If your initialization can run more than once in a frame, make it idempotent:
await page.evaluateOnNewDocument(() => {
if (window.__myHookInstalled) return;
Object.defineProperty(window, '__myHookInstalled', { value: true });
// Install the hook once in this document.
});
That guard is useful when repeated frame execution is not desired; omit it when every new frame should receive independent setup.
Add an external or inline script element
Use page.addScriptTag when the delivery mechanism itself matters—for example, when a library must be loaded from a URL or when you intentionally want inline source represented by a script element.
await page.addScriptTag({
url: 'https://cdn.example.test/library.js',
});
await page.addScriptTag({
content: 'window.injectedFlag = true;',
});
const flag = await page.evaluate(() => window.injectedFlag);
console.log(flag);
The method returns an element handle for the inserted HTMLScriptElement. A URL script still has network and page-policy dependencies, so wait for the library’s expected global or use the returned element when you need to inspect the insertion.
Do these 3 things before closing this tab:
1Scan for outdated or missing drivers - takes under a minute2Clear out junk files and repair common Windows errors3Fix the driver behind crashes, sound loss and screen glitchesRank #3
Target a child frame explicitly
The page shortcut inserts into the main frame. For an iframe, obtain its Frame and call the frame method:
const frame = page.frames().find(f => f.url().includes('/embedded'));
if (!frame) throw new Error('Embedded frame was not found');
await frame.addScriptTag({ content: 'window.frameInjected = true;' });
Do not assume a page-level call injects into every frame.
Expose a Node.js function to page code
page.exposeFunction creates a named function on window. Calls are handled by Puppeteer-side Node.js code, and the page receives a Promise for the returned value. The exposed function remains installed across navigations.
await page.exposeFunction('readBuildInfo', async () => {
return { version: process.env.BUILD_VERSION ?? 'unknown' };
});
await page.evaluate(async () => {
const info = await window.readBuildInfo();
document.body.dataset.buildVersion = info.version;
});
This is a bridge, not unrestricted sharing of Node’s environment. Expose only narrowly scoped operations, validate arguments, and avoid exposing filesystem, shell, credentials, or network capabilities to untrusted page content.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Rank #4
- Brand: Wiley
- Set of 2 Volumes
- A handy two-book set that uniquely combines related technologies Highly visual format and accessible language makes these books highly effective learning tools Perfect for beginning web designers and front-end developers
Timing, navigation, and execution-context rules
Register preload code early
Call evaluateOnNewDocument before page.goto (or before the navigation API you use). Registering after navigation cannot retroactively precede scripts that already ran.
Understand frame repetition
Preload registrations run for future documents, including attached or navigated child frames. A frame-specific API call, by contrast, targets the frame you selected at that moment.
Respect content security policy
Puppeteer documents setBypassCSP and notes that CSP bypassing happens during CSP initialization, usually requiring the call before navigation. CSP behavior remains site- and configuration-dependent; verify it against the target application rather than assuming every injected script will be accepted.
Keep browser and Node boundaries explicit
Browser functions cannot directly import Node modules or read local files. Read a file in Node, pass its source to the browser API, or expose a carefully limited Node function. Likewise, do not return non-serializable browser objects when a plain snapshot of their properties will do.
Best Value
Practical patterns
Extract structured data
const cards = await page.evaluate(() => {
return [...document.querySelectorAll('.card')].map(card => ({
title: card.querySelector('.title')?.textContent?.trim() ?? '',
href: card.querySelector('a')?.href ?? null,
}));
});
Install a diagnostic hook before application code
await page.evaluateOnNewDocument(() => {
const original = console.error;
console.error = (...args) => {
window.__capturedErrors ??= [];
window.__capturedErrors.push(args.map(String).join(' '));
original(...args);
};
});
await page.goto(targetUrl);
Load a library, then use it
await page.addScriptTag({ url: 'https://cdn.example.test/library.js' });
await page.waitForFunction(() => typeof window.ExampleLibrary === 'object');
const version = await page.evaluate(() => window.ExampleLibrary.version);
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Troubleshooting injection failures
| Symptom | Likely cause | Fix |
|---|---|---|
ReferenceError for a Node variable |
The function runs in the browser context | Pass the value as an argument or expose a narrowly scoped Node function |
| Preload code runs too late | Registration happened after navigation | Register with evaluateOnNewDocument before goto or the relevant navigation |
| Hook appears more than once | The preload is invoked for navigations or child frames | Use an initialization guard, or remove the registration when finished |
| Script is missing from an iframe | page.addScriptTag targets the main frame |
Find the intended frame and call frame.addScriptTag |
| External library never becomes available | URL request failure, wrong load order, or page policy | Check the URL, wait for its expected global, inspect page errors, and verify CSP/network behavior |
| Returned value cannot be used in Node | Non-serializable object or context-bound handle | Map it to plain data inside evaluate, or keep and use the handle through Puppeteer’s APIs |
| Exposed function disappears unexpectedly | Name collision or page code overwrote the property | Choose a unique name and check the page’s global before calling it |
| Navigation hangs after injected code | Action and navigation wait were started separately | Use Promise.all with the action and waitForNavigation |
The official Puppeteer references reviewed for these APIs publish no universal compatibility percentage or benchmark for injection methods. Performance depends on the page, script size, network, and browser workload; measure your own flow if latency matters.
Or skip the browser setup
If your goal is a clean screenshot or PDF rather than interactive Puppeteer control, ScreenshotNeo provides a single-call website screenshot API and MCP server. It removes cookie and consent banners, newsletter popups, and chat widgets before capture; bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing result. Its MCP tools—take_screenshot, get_page_info, and capture_pdf—let Claude, Cursor, or another MCP client request captures. The Free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots.
For all options and parameters, see 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
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}`);
Create a free account at ScreenshotNeo sign-up to get 1,000 screenshots each month without a card.
PC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minuteFrequently Asked Questions
Can I inject JavaScript into a page before its first navigation?
Register the preload with page.evaluateOnNewDocument() before calling the navigation method. It applies to documents created by subsequent navigations.
Does page.addScriptTag() inject into every iframe?
No. The page method targets the main frame. Select a child Frame and call that frame’s addScriptTag() method.
How do I stop a preload script?
Keep the identifier returned by evaluateOnNewDocument() and pass its identifier to removeScriptToEvaluateOnNewDocument().
Can page JavaScript call Node.js directly?
Only through an explicit bridge such as page.exposeFunction(). Expose a limited, validated capability rather than broad access to the Node process.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

