Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Rank #2
Sale
HTML and CSS: Design and Build Websites
  • 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Rank #4
Sale
Web Design with HTML, CSS, JavaScript and jQuery Set
  • 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.Support on Ko-Fi

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Frequently 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.