How to Expose a Node.js Function to the Browser with Puppeteer
Expose a Node.js callback as a page function with Puppeteer, call it from browser code, and handle navigation, cleanup, and common errors.
Use Puppeteer’s page.exposeFunction(name, callback) to make a Node.js callback available as a function on the controlled page’s window. Browser code calls that function and receives a Promise for the callback’s result. Register the bridge from Node.js, then invoke it from page-context code such as page.evaluate().
The distinction matters: the callback passed to exposeFunction() runs in Node.js; the function passed to evaluate() runs in the browser page. Puppeteer’s Page.exposeFunction() API reference describes the method as adding a function called name to the page’s window.
Runnable example: expose a Node.js function
This example exposes a function that computes an MD5 digest using Node’s built-in crypto module. Save it as expose-function.cjs and run it in a project that has Puppeteer installed.
const puppeteer = require('puppeteer');
const { createHash } = require('node:crypto');
async function main() {
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
// This callback is registered and executed in the Node.js process.
await page.exposeFunction('md5', (text) =>
createHash('md5').update(text).digest('hex')
);
// This function runs in the browser page. window.md5 calls back to Node.
const digest = await page.evaluate(async () => {
return await window.md5('PUPPETEER');
});
console.log(digest);
} finally {
await browser.close();
}
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
Install Puppeteer if it is not already in the project with npm install puppeteer. The package normally downloads a compatible browser during installation. If your environment manages Chrome separately, see Puppeteer’s installation guide and configure launch options for that environment.
How the bridge works
- Node registers the callback. Call and await
page.exposeFunction('name', callback)on the specificPagethat needs it. - Puppeteer adds a page function. Page code can call it as
window.name(...args). - The callback runs in Node. Its return value is sent back to the page asynchronously; if the callback returns a Promise, the page-side call waits for that Promise.
- Page code handles the result. Use
awaitor.then()in browser code. When called insidepage.evaluate(), return or await the value so Puppeteer can return it to Node.
page.evaluate() is one way for the Node caller to ask the page to execute code. The function you pass to it is serialized and executed in the page context; it does not make its body a Node.js callback. See the Page.evaluate() API reference.
Async callbacks and error handling
An exposed callback can perform asynchronous Node work. For example, Puppeteer’s API documentation demonstrates exposing a file-reading function. Here is a complete pattern using Node’s promise-based file API:
const puppeteer = require('puppeteer');
const { readFile } = require('node:fs/promises');
async function main() {
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
await page.exposeFunction('readTextFile', async (path) => {
const contents = await readFile(path, 'utf8');
return contents;
});
const contents = await page.evaluate(async () => {
return await window.readTextFile('/tmp/example.txt');
});
console.log(contents);
} finally {
await browser.close();
}
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
This is a mechanics example, not a recommendation to let untrusted page code choose arbitrary file paths. Treat an exposed function as a capability granted to code running in that page. Validate arguments, restrict operations and paths, and avoid exposing secrets or unrestricted filesystem, network, or process access. The browser page should receive only the minimum result it needs.
Handle failures at the appropriate boundary. Put try/catch inside the exposed callback for expected Node-side failures and return a deliberate, serializable error result if the page should handle it as data. Put try/catch around the browser-side call to handle a rejected Promise, and around page.evaluate() in Node to handle evaluation or page lifecycle failures. Do not assume every error has identical serialization behavior across Puppeteer versions; consult the API documentation matching the version in your project.
Arguments, return values, and scope
Pass data as arguments
Values needed by page code should be passed as arguments to page.evaluate(), or supplied to the exposed function call. A function passed to evaluate() cannot use ordinary Node lexical variables: it executes in the page, not in the Node module. For example:
const prefix = 'processed:';
await page.exposeFunction('formatInNode', async (value) => {
return `${prefix} ${String(value)}`;
});
const result = await page.evaluate(async (input) => {
return window.formatInNode(input);
}, 'hello');
Keep the values simple
Use ordinary data such as strings, numbers, booleans, arrays, and plain objects as the bridge’s inputs and outputs. Browser and Node contexts do not share object identity or live references. Avoid passing DOM nodes, functions, or complex runtime objects across the boundary; transform the information into plain data on the side where it lives. The supplied research does not establish every serialization edge case, so check the documentation for your installed Puppeteer version if you depend on a less common value type.
Register on the right page
An exposed function belongs to the Page on which it is registered. A browser can have multiple pages, and registering on one page does not register on another. Register separately on each page that needs the bridge.
Navigation and removing an exposed function
Exposed functions survive page navigations. Register the function before the page code needs it, then navigate as required. To remove a previously exposed function, call page.removeExposedFunction(name):
await page.exposeFunction('getValue', async () => 'available');
await page.goto('https://example.com');
// The exposed function remains available after navigation.
const value = await page.evaluate(() => window.getValue());
await page.removeExposedFunction('getValue');
See the official Page.removeExposedFunction() reference. If a page is replaced with a newly created Page, register the function on that new page as well.
Choosing between exposeFunction and evaluate
| Call | Runs where? | Use it for |
|---|---|---|
page.exposeFunction(name, callback) |
The callback runs in Node.js; the exposed name is called from the page. | Giving page code a narrow, named way to request Node-side work. |
page.evaluate(pageFunction, ...args) |
The supplied function runs in the browser page. | Reading or changing page state and returning a result to Node. |
Use evaluate() alone when all work belongs in the page and its result can be returned to Node. Add exposeFunction() when page code must request an operation that only the Node process can perform. This division keeps the execution context clear.
Common problems and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
window.myFunction is missing |
The function was not exposed on this Page, registration was not awaited, or the call targets a different page. |
Await page.exposeFunction() before invoking it and verify that registration and evaluation use the same Page. |
A Node variable is undefined inside page.evaluate() |
The evaluation callback runs in the page context and cannot close over the Node module’s lexical scope. | Pass the value as an evaluate() argument or use an exposed function for Node-side work. |
| The page continues before the Node result is ready | The browser call was not awaited. | Use await window.myFunction(...) or return its Promise from the evaluation callback. |
| The operation works until navigation | The call may be running on a different or newly created Page, or code ran before the bridge was registered. |
Register on the page that owns the context and await registration before the dependent page code runs. Exposed functions survive navigation on the same page. |
| The callback rejects or page evaluation fails | Node-side work threw or rejected, or the page context was unavailable during evaluation. | Log and handle expected Node failures in the callback, catch the page-side Promise rejection, and catch the corresponding evaluate() failure in Node. |
| Unexpected values arrive across the bridge | The value may not be a simple serializable value or may not behave as a shared object. | Convert to plain data before returning it. Check the installed-version docs for any value type on which the application depends. |
| The browser process remains open after an error | Cleanup was skipped on an exceptional path. | Place browser.close() in a finally block, as in the examples. |
Performance, reliability, and operational notes
- Keep calls purposeful. Each bridge call crosses between the page and Node and completes asynchronously. Batch related data into one call when practical instead of making many calls for tiny values.
- Bound slow work. A page-side
awaitremains pending until the callback’s returned Promise settles. Ensure callbacks have clear failure paths and do not leave promises unresolved. - Limit authority. An exposed function gives page code access to behavior in the Node process. Validate inputs and expose small operations rather than general-purpose filesystem or process access.
- Clean up resources. Close the browser in a
finallyblock and remove exposed functions when they are no longer needed but the page remains in use. - Pin and check versions. API references can change across Puppeteer versions. Match the docs to the version installed by the project lockfile, especially when relying on less common behavior.
No benchmark or per-call cost figure is established by the source material for this guide. Measure the complete workflow in its actual environment if latency or throughput matters.
Or skip the browser setup
If the goal is a screenshot rather than custom page-to-Node behavior, ScreenshotNeo provides a website screenshot API and MCP server. A GET request takes a URL and returns an image or PDF; the API and options are documented at ScreenshotNeo’s API docs.
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
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, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server offers take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots.
Create a free ScreenshotNeo account and get 1,000 screenshots a month with no card.
FAQ
Can an exposed function return a Promise?
Yes. The page-side call returns a Promise, and Puppeteer waits for a Promise returned by the Node callback.
Does exposing a function make it available to every tab?
No. Register it on each Page that needs the function.
Does the exposed function remain after navigation?
Yes, the documented behavior is that exposed functions survive navigations on that page. Remove one with page.removeExposedFunction(name).
Can I use this to call Node from an ordinary website without Puppeteer?
This bridge is provided by Puppeteer for the page it controls. It is not a general browser feature available to arbitrary visitors.


