Free tools Windows power users keep installed
One-click scans. No signup required.
The short answer: pass a Node.js value as an argument to page.evaluate() when you need it for one operation. If the page must see a global before its own scripts run—and after every navigation—register the assignment with page.evaluateOnNewDocument(). If browser code must call Node.js on demand, use page.exposeFunction().
These APIs solve different timing, lifetime and direction problems. Choosing the right one prevents globals that appear too late, disappear after navigation, or expose more Node.js capability than intended.
How Puppeteer contexts work
Puppeteer controls a browser, but a function supplied to page.evaluate() executes in the page’s JavaScript context, not in Node.js. The two contexts do not share ordinary lexical variables. A Node variable such as config is unavailable inside the browser function unless you pass it as an argument or deliberately create a bridge.
Values crossing the boundary should be JSON-like data: strings, numbers, booleans, arrays, plain objects and null. Functions, class instances, sockets and many host objects do not serialize as ordinary configuration. Pass only the fields the page needs.
Recommended Free Tools
#1 Best Overall
1. Inject a value for one operation with page.evaluate
Use an explicit argument when the page already exists and the value is needed by one calculation or interaction.
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://example.test');
const config = {
apiBase: 'https://api.example.test',
featureFlag: true
};
const result = await page.evaluate((cfg) => {
window.appConfig = cfg;
return {
enabled: window.appConfig.featureFlag,
api: window.appConfig.apiBase
};
}, config);
console.log(result);
await browser.close();
The second argument, config, is serialized and supplied to the page function as cfg. Puppeteer waits for a promise returned by the function, so asynchronous page work can be awaited inside it.
Use a return value instead of a permanent global when possible
If the value is needed only to calculate a result, avoid changing window:
const title = await page.evaluate((prefix) => {
return `${prefix}: ${document.title}`;
}, 'Captured page');
This keeps page state smaller and makes tests easier to reason about. Assign to window only when application code or later evaluations genuinely need a global.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Rank #2
Why closing over Node variables fails
const config = { featureFlag: true };
// Do not rely on this: config is a Node.js variable.
await page.evaluate(() => window.appConfig = config);
The function is transferred to the browser context; its Node.js closure is not. Pass config explicitly, as in the working example.
2. Install a global before site scripts with evaluateOnNewDocument
Use page.evaluateOnNewDocument() when application JavaScript must see the value during startup. Puppeteer runs the supplied function after a document is created but before that document’s scripts execute. Register it before goto().
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
const page = await browser.newPage();
const config = {
apiBase: 'https://api.example.test',
featureFlag: true
};
await page.evaluateOnNewDocument((cfg) => {
window.appConfig = cfg;
}, config);
await page.goto('https://example.test', { waitUntil: 'domcontentloaded' });
const flagSeenByPage = await page.evaluate(() => window.appConfig.featureFlag);
console.log(flagSeenByPage); // true
await browser.close();
This hook is invoked for navigations and for child frames when they are attached or navigated. It is therefore the appropriate choice for a global that must be installed repeatedly, rather than assigned once to a document that may soon be replaced.
Navigation replaces a document
A one-time assignment made with page.evaluate() belongs to the current document. A full navigation creates a new document and can remove that assignment:
Quick wins for a faster PC:
Repair Windows errors before they cause bigger problemsFix Now →Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Rank #3
await page.goto('https://example.test/first');
await page.evaluate(() => { window.appConfig = { featureFlag: true }; });
await page.goto('https://example.test/second');
// The second document does not inherit the first document's window object.
Register the new-document hook before the first navigation (or before later navigations if the page already exists) to reinstall the global.
Ordering and changing configuration
Each call captures the argument supplied when you register it. If configuration changes, register a new hook or use a page evaluation after navigation. For deterministic startup, register all required hooks before triggering navigation, reloads or actions that cause a new document.
3. Let page code call Node.js with exposeFunction
Sometimes the browser should request current data rather than receive a copied snapshot. page.exposeFunction(name, callback) adds a function with that name to the page’s window. The callback runs in Node.js, and a returned promise is awaited. The exposed function survives navigations.
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
const page = await browser.newPage();
const config = { apiBase: 'https://api.example.test', featureFlag: true };
await page.exposeFunction('getAppConfig', async () => {
return config;
});
await page.goto('https://example.test');
const value = await page.evaluate(() => window.getAppConfig());
console.log(value);
await page.goto('https://example.test/another-route');
const valueAfterNavigation = await page.evaluate(() => window.getAppConfig());
console.log(valueAfterNavigation);
await browser.close();
This is a callable bridge, not a passive global. The browser invokes it and receives a serialized result. Because the callback can perform Node.js work, expose only capabilities that the page is meant to use.
Validate arguments at the Node boundary
await page.exposeFunction('lookupFeature', async (name) => {
if (typeof name !== 'string' || !/^[a-z0-9_-]+$/i.test(name)) {
throw new Error('Invalid feature name');
}
return config.featureFlag && name === 'new-dashboard';
});
Any script able to call the named window function can invoke the callback. Choose a distinctive name, validate every argument, return the smallest useful result, and avoid exposing file-system, process or network operations without strict checks.
Which API should you choose?
| Need | API | Timing and lifetime | Direction |
|---|---|---|---|
| One operation or small calculation | page.evaluate(fn, value) |
Runs when called in the current document | Node value to page function |
| Global or shim before application scripts | page.evaluateOnNewDocument(fn, value) |
Runs before page scripts on navigations and child-frame document creation | Node value to every new document |
| Page requests live Node.js data or an action | page.exposeFunction(name, callback) |
Window function persists across navigations | Page call to Node callback and result back |
A practical decision sequence
- Ask whether the page must see the value before its own scripts run. If yes, use
evaluateOnNewDocument. - If timing is not critical and the value is used once, pass it to
evaluate. - If the page needs to request changing data or a Node-side operation, expose a narrowly scoped function.
- For frames, decide whether the global belongs in every child frame. New-document hooks apply there; a top-level
evaluateassignment does not automatically populate another frame.
Serialization, frames and security edge cases
Keep payloads serializable and minimal
Convert configuration to plain data before crossing contexts. Instead of passing a class instance, send the required fields. Do not put secrets in a browser global: page scripts, third-party scripts and extensions with page access may read it. Prefer short-lived values and server-side authorization where possible.
Top frame versus child frames
page.evaluate() runs in the page’s selected frame. To target another frame, obtain the frame and evaluate there. A new-document hook is applied when child frames are attached or navigated, so it is the safer mechanism when every frame must receive the same bootstrap value. Cross-origin restrictions still apply to what your page code can access inside a frame.
Reloads, redirects and single-page routing
Reloads and server redirects create documents and trigger the new-document hook. Client-side route changes in a single-page application may not create a document; the existing global remains unless application code changes it. If a route-specific value is required, update it deliberately with page.evaluate or have page code call an exposed function.
The Tool Desk
Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Best Value
Common failures and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
ReferenceError: config is not defined |
A page function tried to close over a Node variable. | Pass it as an argument: page.evaluate(fn, config). |
Application reads undefined during startup |
The global was assigned after page scripts ran. | Register evaluateOnNewDocument before navigation. |
Global disappears after goto or reload |
The old document was replaced. | Install a new-document hook; do not rely on a one-time assignment. |
| Exposed function is missing | It was called before registration, or a different page/browser context is being used. | Await page.exposeFunction before navigation and call it on that same page. |
| Callback receives unexpected input | Browser code can call the bridge with arbitrary values. | Validate types and allowed values in Node.js and throw on invalid input. |
| Value is present in the top page but not an iframe | The evaluation targeted only one frame. | Evaluate in the intended frame or use a new-document hook for frame bootstrapping. |
| Serialization error or missing fields | The payload contains unsupported objects or more data than intended. | Map it to a plain JSON-like object before passing it. |
Testing and operational guidance
- Test a first load, a full reload, a redirect and a second URL when persistence matters.
- Assert the value from inside the page with
page.evaluate, rather than trusting Node’s original object. - Test child frames separately if the site embeds them.
- Register hooks before any action that can navigate, including clicks that submit forms.
- Log the hook registration point and navigation URL when diagnosing race conditions; avoid logging secret values.
- Keep exposed callbacks fast and bounded. If they perform asynchronous work, handle rejection so page code receives a predictable error.
Or skip the browser setup
If your goal is a clean image or PDF rather than browser automation, ScreenshotNeo accepts one GET request and returns a PNG, JPEG, WebP or PDF. It can accept cookie and consent banners before capture, remove more than 60 known consent platforms plus newsletter popups and chat widgets, and lets you turn those steps off. Only clean shots are billed; bot checks or CAPTCHAs, blank pages, timeouts, failed loads and cache hits are not billed, with the outcome reported in X-Page-Verdict and X-Billed headers. Its MCP server provides take_screenshot, get_page_info and capture_pdf tools for Claude, Cursor and other MCP clients.
See the complete parameter reference in the ScreenshotNeo documentation. A basic request is:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Equivalent 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)
Equivalent 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}`);
Every feature is included on every plan. The Free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account to start.
FAQ
Does evaluateOnNewDocument run before every script?
It runs after the document is created and before that document’s scripts execute, including newly created or navigated child frames.
Can an exposed function return a promise?
Yes. Puppeteer awaits the returned promise and delivers its resolved value to the page, or reports its rejection as an error.
Should secrets be injected as globals?
No. Anything placed on window can be inspected by page JavaScript. Keep secrets in Node.js and expose only narrowly validated operations when necessary.
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.




