Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.
Fix these failures by identifying which contract you are breaking. page.exposeFunction() installs a browser-side window function whose callback runs in Node.js; it is asynchronous and must be awaited. A selector lookup is different: page.$() returns the first matching element or null, while native document.querySelector() either returns an element, returns null, or throws for invalid CSS. Frame context, render timing, navigation, and the exact Puppeteer version determine the correct fix.
The current Puppeteer API pages display version 25.12.0. The cross-site iframe teardown problem described below is a report against 25.5.0, so treat it as version- and lifecycle-specific rather than a universal defect.
Start with the symptom, not the workaround
| Symptom | What it means | First check |
|---|---|---|
page.$() returns null |
No matching node was found in the page’s main frame at that instant. | Selector spelling, render timing, frame, and shadow-root location. |
document.querySelector() throws |
The browser rejected the selector as invalid CSS. | Remove Puppeteer-only selector prefixes and validate CSS syntax. |
exposeFunction rejects with Target closed |
Binding installation lost a protocol target, often during frame teardown. | Frame churn, navigation timing, installed version, and whether the main-frame binding still exists. |
| Exposed function is undefined in page code | Installation may not have completed, or the call is in the wrong context. | Await installation and call window.name from a page evaluation. |
Record the exact Puppeteer version, browser (for example, bundled Chromium or a system Chrome), URL, frame structure, navigation sequence, and complete error text before changing code. These details separate a deterministic selector mistake from a timing or protocol race.
Use page.exposeFunction with the documented contract
The API adds a function to the page’s window. The callback executes in Node.js, and the value returned by the callback (including a Promise) is awaited before the page receives the result. Puppeteer documents that “Functions installed via page.exposeFunction survive navigations.” The installation call itself returns a Promise, so await it before evaluating page code.
#1 Best Overall
- Compatible with Nintendo Switch 2’s new GameChat mode
- Auto-Light Balance: RightLight boosts brightness by up to 50%, reducing shadows so you look your best—compared to previous-generation Logitech webcams (1)
- Privacy with a Slide: The integrated webcam cover makes it easy to get total, reliable privacy when you're not on a video call
- Built-In Mic: The built-in microphone lets others hear you clearly during video calls
- Easy Plug-And-Play: The Brio 101 works with most video calling platforms, including Microsoft Teams, Zoom and Google Meet—no hassle; it just works
Minimal working example
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.exposeFunction('readFromNode', async (value) => {
// This function runs in Node.js, not in the browser.
return `Node received: ${String(value)}`;
});
await page.setContent('<!doctype html><title>Bridge test</title>');
const result = await page.evaluate(async () => {
// The exposed function is a window property in the page.
return await window.readFromNode('hello');
});
console.log(result); // Node received: hello
await browser.close();
Pass data that can be serialized across the DevTools Protocol boundary. Keep filesystem, database, and other Node-only operations inside the callback; page JavaScript cannot directly access them. From page code, call the function through window and await its Promise.
Installation order and navigation
- Create the page or obtain the target frame.
- Call and await
page.exposeFunction(). - Navigate or set content.
- Invoke the function from
page.evaluate()or application code.
Because the official reference says bindings survive navigations, reinstalling after every goto() is normally unnecessary. If your setup contradicts that statement, capture a minimal reproduction including version, browser, navigation order, and whether invocation occurred before or after navigation. An older report about post-navigation exposure is not a reason to override the current API contract.
Diagnose Protocol error (Runtime.addBinding): Target closed
GitHub issue #15299 (opened August 6, 2026) reports this rejection on Puppeteer 25.5.0 with Node.js 22.20.0 on Windows. The reproduction creates eight cross-site iframes and removes them while page.exposeFunction() is distributing the binding. The report attributes the error to an out-of-process iframe target closing during installation. In that reproduction, the main-frame binding remained a function and a later evaluation returned 42; the page was still usable.
Check for partial success before recovery
try {
await page.exposeFunction('hostPing', () => 'ok');
} catch (error) {
console.error('exposeFunction failed:', error);
const state = await page.evaluate(() => ({
type: typeof window.hostPing,
result: typeof window.hostPing === 'function' ? window.hostPing() : null
})).catch(e => ({ evaluationError: String(e) }));
console.log('main-frame binding state:', state);
}
This check tells you whether the main frame is healthy; it does not prove every child frame received the binding. Enumerate frames and inspect their lifecycle before deciding that installation failed completely.
Rank #2
- Compatible with Nintendo Switch 2’s new GameChat mode
- Crisp HD 720p/30 fps video calls with diagonal 55° field of view and auto light correction. Compatible with popular platforms including Skype and Zoom.
- The built-in noise-reducing mic makes sure your voice comes across clearly up to 1.5 meters away, even if you’re in busy surroundings.
- C270’s RightLight 2 feature adjusts to lighting conditions, producing brighter, contrasted images to help you look good in all your conference calls.
- The adjustable universal clip lets you attach the camera securely to your screen or laptop, or fold the clip and set the webcam on a shelf. You’re always ready for your next video call.
Make frame churn observable
page.on('frameattached', frame => {
console.log('attached', frame.url());
});
page.on('framedetached', frame => {
console.log('detached', frame.url());
});
page.on('framenavigated', frame => {
console.log('navigated', frame.url());
});
Temporarily delay iframe creation/removal, or expose the function before the application starts creating cross-site frames, to determine whether teardown is the trigger. Do not blindly retry: a second call can race with the same lifecycle and may produce duplicate-binding errors or another partial state. Only adopt a retry after reproducing it with your site’s frame behavior.
Verify the installed version before applying issue advice
npm ls puppeteer
# or
node -p "require('puppeteer/package.json').version"
The issue author reports a bisect in which 22.12.1 resolved the reproduction and 22.13.0 threw, while the issue itself lists 25.5.0. Those are the author’s observations, not a compatibility guarantee. Compare your exact version and browser, then test a controlled upgrade or downgrade in a lockfile-based branch. Preserve a reproduction and check release notes before changing production dependencies.
Fix page.$() returning null
The official page.$() method searches the page’s main frame and resolves to the first matching element or null. Null is a valid result, not an exception. The error usually occurs later when code dereferences it.
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 →Handle optional and required elements explicitly
const card = await page.$('.product-card');
if (!card) {
throw new Error('Required .product-card was not rendered');
}
await card.click();
For an optional element, branch instead of throwing. For a required element rendered asynchronously, wait for the application’s readiness condition and then query:
Rank #3
- 【Full HD 1080P Webcam】Powered by a 1080p FHD two-MP CMOS, the NexiGo N60 Webcam produces exceptionally sharp and clear videos at resolutions up to 1920 x 1080 with 30fps. The 3.6mm glass lens provides a crisp image at fixed distances and is optimized between 19.6 inches to 13 feet, making it ideal for almost any indoor use.
- 【Wide Compatibility】Works with USB 2.0/3.0, no additional drivers required. Ready to use in approximately one minute or less on any compatible device. Compatible with Mac OS X 10.7 and higher / Windows 7, 8, 10 & 11 / Android 4.0 or higher / Linux 2.6.24 / Chrome OS 29.0.1547 / Ubuntu Version 10.04 or above. Not compatible with XBOX/PS4/PS5.
- 【Built-in Noise-Cancelling Microphone】The built-in noise-canceling microphone reduces ambient noise to enhance the sound quality of your video. Great for Zoom / Facetime / Video Calling / OBS / Twitch / Facebook / YouTube / Conferencing / Gaming / Streaming / Recording / Online School.
- 【USB Webcam with Privacy Protection Cover】The privacy cover blocks the lens when the webcam is not in use. It's perfect to help provide security and peace of mind to anyone, from individuals to large companies. 【Note:】Please contact our support for firmware update if you have noticed any audio delays.
- 【Wide Compatibility】Works with USB 2.0/3.0, no additional drivers required. Ready to use in approximately one minute or less on any compatible device. Compatible with Mac OS X 10.7 and higher / Windows 7, 10 & 11, Pro / Android 4.0 or higher / Linux 2.6.24 / Chrome OS 29.0.1547 / Ubuntu Version 10.04 or above. Not compatible with XBOX/PS4/PS5.
await page.waitForSelector('.product-card', { visible: true });
const card = await page.$('.product-card');
if (!card) throw new Error('Selector disappeared after wait');
Use a timeout appropriate to your application and catch timeout errors at the test boundary. Navigation completion alone does not guarantee that client-side rendering has finished.
Log the exact selector and inspect the DOM
const selector = '[data-testid="checkout-submit"]';
console.log('selector:', JSON.stringify(selector));
console.log('url:', page.url());
console.log('matches:', await page.$$eval(selector, nodes => nodes.length));
console.log('html:', await page.content());
Confirm that the selector string is exactly the one used in production code. Check spelling, escaping, case sensitivity, and whether a framework replaced the node after your query.
Understand CSS selectors versus Puppeteer selectors
Puppeteer accepts ordinary CSS selectors and also supports Puppeteer-specific syntax for text, accessibility role/name, XPath, and combinations that cross shadow roots. Native document.querySelector() understands only browser CSS selector semantics. A Puppeteer-specific prefix that works with page.$() should not be pasted into a page evaluation.
Use each API in its own context
// Puppeteer selector syntax (evaluated by Puppeteer)
const button = await page.$('::-p-text(Continue)');
// Native DOM CSS syntax (evaluated by the browser)
const nativeButton = await page.evaluate(() =>
document.querySelector('button.continue')
);
If native lookup throws, validate CSS syntax first. If it returns null, inspect whether the node exists in that document, appears only after rendering, or lives in another frame or shadow root.
Rank #4
- 1080P Webcam with Cover for Video Calls - EMEET computer webcam provides design and Optimization for professional video streaming. Realistic 1920 x 1080p video, 5-layer anti-glare lens, providing smooth video. C960 computer camera delivers 1920x1080 video with fixed focus (11.8–118.1 inches), so as to provide a clearer image. C960 USB webcam has a cover and can be removed automatically to meet your needs for privacy. For optimal image performance, use the webcam in a well-lit environment.
- Built-in 2 Omnidirectional Mics - EMEET webcam with microphone for desktop features 2 built-in omnidirectional microphones, picking up your voice to create clear audio for communication. When installing the webcam, select EMEET C960 as the default microphone input device in your computer and video applications and select C960 as the default device in Zoom/Teams and ensure microphone permissions are enabled for proper use. Please note that C960 does not include built-in speakers.
- Automatic Light Adjustment - Automatic exposure adjustment is applied in EMEET HD webcam 1080p so that the streaming webcam can deliver stable image performance. EMEET C960 camera for computer also features color adjustment and exposure optimization to help you look your best. For optimal video quality, it is recommended to use the webcam in normal or well-lit environments and select suitable video settings in your application. Proper lighting helps achieve a clearer and more balanced image.
- Plug-and-Play & Upgraded USB Connectivity - New C960 webcam features both USB Type-A & A-to-C adapter connections for wider compatibility. For stable performance, connect the webcam directly to the computer's main USB port and ensure the device is recognized correctly. If a hub or docking station is used, please ensure it provides sufficient power and stable data transmission, as limited ports may affect performance. 90° wide-angle lens captures more participants without frequent adjustments.
- High Compatibility & Multi Application - C960 webcam for laptop is compatible with Windows 10/11, macOS 10.14+, and Android TV 7.0+. Not supported: Windows Hello, TVs, tablets, or game consoles. It works with Zoom, Teams, Facetime, Google Meet, YouTube and more. Please select C960 webcam as the default camera and microphone device in your application and ensure camera/microphone permissions are enabled, especially on macOS. (Tips: Incompatible with Windows Hello)
Do not mix frame contexts
page.$() is a shortcut for the main frame’s selector method. An element inside an iframe must be queried through that frame:
const frame = page.frames().find(f => f.url().includes('/checkout'));
if (!frame) throw new Error('Checkout frame not attached');
await frame.waitForSelector('#pay');
const pay = await frame.$('#pay');
if (!pay) throw new Error('#pay missing in checkout frame');
For a shadow root, use Puppeteer’s shadow-root-capable selector syntax or run native DOM code from the correct host context. A selector cannot cross a browsing-context boundary simply because the iframe is visible on screen.
A repeatable troubleshooting workflow
- Capture facts: Puppeteer version, browser version, operating system, URL, frame URLs, navigation steps, exact selector or exposed-function name, and full error/result.
- Classify the contract: bridge installation, selector result, native selector exception, or a later dereference error.
- Pin the context: Node callback, main frame, child iframe, or shadow root.
- Pin the time: before navigation, after navigation, after a render signal, or while frames attach/detach.
- Reduce the case: remove unrelated scripts and iframes; retain only the failing call.
- Verify health: evaluate a harmless expression such as
2 + 40, inspect the binding type, and list current frames. - Change one variable: selector, wait condition, frame context, or dependency version—not all at once.
Common errors and targeted fixes
| Error or result | Likely cause | Fix |
|---|---|---|
window.myFn is not a function |
Evaluation ran before exposure completed, or the name differs. | Await page.exposeFunction(); verify the exact name and call window.myFn. |
Target closed during exposure |
Target/frame detached during protocol binding installation. | Log frame events, inspect main-frame state, reduce iframe churn, and test your exact version. |
Cannot read properties of null |
page.$() returned null. |
Check the result before dereferencing; wait for the real render condition. |
Failed to execute 'querySelector' |
Invalid CSS or Puppeteer-only syntax passed to native DOM code. | Validate CSS and use native syntax inside document.querySelector. |
| Selector works manually but not in automation | Wrong frame, shadow root, dynamic rendering, or changed DOM. | Print frame URLs, inspect HTML at query time, and use the appropriate frame/shadow selector. |
| Binding disappears after a custom reload | Contradicts the documented navigation-survival contract or uses a different target/page. | Confirm the page object and version; create a minimal reproduction rather than repeatedly reinstalling. |
Performance and reliability considerations
Every exposed call crosses the browser–Node protocol boundary, so batch data when practical rather than making hundreds of tiny calls from page code. Return compact, serializable values and keep expensive Node work asynchronous. For selectors, prefer stable attributes such as data-testid over generated class names, and wait on a meaningful application signal instead of a fixed sleep. Fixed delays can hide slow-page failures and waste time on fast pages.
Frame-heavy pages deserve explicit lifecycle handling. Install bindings before predictable iframe creation when possible, listen for attachment and detachment, and treat a rejected exposure as a state to investigate—not proof that the browser is unusable. Keep dependency versions locked while diagnosing, then test upgrades and downgrades against the reduced reproduction.
Best Value
- Compatible with Nintendo Switch 2’s new GameChat mode
- HD lighting adjustment and autofocus: The Logitech webcam automatically fine-tunes the lighting, producing bright, razor-sharp images even in low-light settings. This makes it a great webcam for streaming and an ideal web camera for laptop use
- Advanced capture software: Easily create and share video content with this Logitech camera that is suitable for use as a desktop computer camera or a monitor webcam
- Stereo audio with dual mics: Capture natural sound during calls and recorded videos with this 1080p webcam, great as a video conference camera or a computer webcam
- Full HD 1080p video calling and recording at 30 fps. You'll make a strong impression with this PC webcam that features crisp, clearly detailed, and vibrantly colored video
Or skip the browser setup
If your goal is a clean image or PDF rather than interactive automation, ScreenshotNeo provides a one-request screenshot API and an MCP server for AI agents. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be disabled. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status.
Use the API documentation at https://screenshotneo.com/docs/ for options such as full-page lazy-image loading, CSS-selector element capture, device and retina settings, PDFs, custom CSS/JavaScript, clicks, waits, request blocking, headers/cookies, geolocation, signed links, asynchronous webhooks, bulk capture, caching, and usage reporting.
One-call cURL example
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
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)
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}`);
ScreenshotNeo includes take_screenshot, get_page_info, and capture_pdf tools through its MCP server for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 shots per month without a card; paid plans start at $5 for 3,000 shots. Create an account at https://screenshotneo.com/account/sign-up/.
Recommended Free Tools
Frequently Asked Questions
Should I use page.$() or document.querySelector()?
Use page.$() when you need Puppeteer’s selector features, element handles, or frame-aware automation. Use document.querySelector() inside page.evaluate() when you specifically need native browser CSS semantics.
Does exposeFunction need to be called again after every navigation?
The current Puppeteer API reference says exposed functions survive navigations. If yours does not, verify that you are evaluating the same page, record the exact version and navigation sequence, and reduce the case before adding reinstall logic.
Can an exposed callback access browser globals such as document?
No. The callback runs in Node.js. Browser globals belong in page code; pass serializable arguments to the Node callback and return a serializable result.
Why does a selector work in DevTools but fail in Puppeteer?
DevTools may be inspecting a different frame or a later DOM state. Log the frame URL and query at the same time your script runs, then wait for the application’s render condition.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
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.

