How to Debug Puppeteer Scripts
Find which Puppeteer operation failed, collect the right evidence, and fix browser, page, protocol, and timeout errors without hiding failures.
To debug Puppeteer scripts, first identify the exact operation that failed and preserve its complete error and stack trace. Then choose a diagnostic method for the failing boundary: Node.js orchestration, browser page code, browser startup, or the DevTools Protocol. Change one relevant setting or wait condition at a time, rerun the smallest failing sequence, and rethrow errors after logging so failures cannot masquerade as successful results.
The official Puppeteer debugging guide is published under /next/; commands and options can vary by installed release. Check the documentation for your version before applying a fix.
1. Preserve the failure before changing anything
Record enough information to reproduce the problem. A final error line alone may omit the operation, frame, request, or earlier exception that explains it.
- Save the complete error message and stack trace.
- Record the installed Puppeteer version, browser version, Node.js version, operating system, and relevant launch options.
- Note the operation active at failure: launch, navigation, selector wait, click, evaluation, request interception, or close.
- Log useful context, such as a redacted hostname, selector, and wait condition. Do not log credentials, cookies, page contents, or sensitive URL query parameters.
Log and rethrow an error. Returning an empty value can make an automation job look successful even though it failed:
try {
await page.goto('https://example.com', {waitUntil: 'domcontentloaded'});
await page.locator('h1').wait();
} catch (error) {
console.error('Puppeteer operation failed:', error);
throw error;
}
Use the methods in this guide to add evidence, then keep the original failure visible to the caller or job runner.
2. Locate the failing phase
Puppeteer crosses several boundaries: the Node.js process, the browser process, a page’s JavaScript context, and the protocol connecting them. Start with the phase that failed rather than changing unrelated settings.
| Failure boundary | First checks |
|---|---|
| Before the browser starts | Browser installation, cache path, executable configuration, platform dependencies, and sandbox setup. |
| Opening a page | Navigation error, redirects, response status, and the exact navigation condition being awaited. |
| Waiting for content | Whether the selector or page-state condition matches the content the task actually needs. |
| After an iframe or element changes | Whether the frame is still current and whether element handles need to be reacquired. |
| Clicking or filling | Whether the target is the expected element type and is visible and actionable. |
| Request interception | Whether every intercepted request is handled exactly once. |
| Async call hangs or target disappears | Protocol diagnostics and whether the page, browser, or target was closed while the operation was pending. |
For navigation failures, preserve the URL in redacted form, the thrown error, and the configured waitUntil condition. If navigation resolves but the expected content is missing, inspect the response and redirects and verify that the page-state wait describes the desired state.
3. Inspect what the browser displays
Run with a visible browser when you need to see the actual page and operation order. Puppeteer’s guide also shows slowMo: 250 milliseconds as an example delay; it is a debugging aid, not a recommended production setting.
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({
headless: false,
slowMo: 250,
});
try {
const page = await browser.newPage();
page.on('console', message => {
console.log(`PAGE ${message.type()}: ${message.text()}`);
});
await page.goto('https://example.com', {waitUntil: 'domcontentloaded'});
await page.locator('h1').wait();
} finally {
await browser.close();
}
})().catch(error => {
console.error(error);
process.exitCode = 1;
});
Page console output does not automatically appear in Node.js. The page.on('console', ...) listener forwards it. To stop inside browser-side code, launch with devtools: true and put a debugger statement in the code evaluated in the page:
const browser = await puppeteer.launch({headless: false, devtools: true});
const page = await browser.newPage();
await page.evaluate(() => {
debugger;
// Inspect the page context in DevTools from here.
return document.title;
});
Use this for client-side code and DOM state. It will not pause Node.js orchestration code; use the Node inspector for that.
4. Step through the Node.js script
Put a debugger statement in the Node script and start it with the inspector paused at the beginning:
debugger;
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://example.com');
node --inspect-brk path/to/script.js
In Chrome or Chromium, open chrome://inspect/#devices, inspect the Node process, and resume execution with F8. This lets you step through awaited Puppeteer calls while observing browser behavior. The documented workflow is scoped to Chrome/Chromium. Due to a Chromium bug noted in the guide, an awaited page action cannot be run directly in the DevTools console; put experiments in the test file instead.
5. Collect browser-process and protocol evidence
When Chrome crashes or cannot start, dumpio: true forwards browser process output to Node’s standard streams:
const browser = await puppeteer.launch({dumpio: true});
For lower-level protocol traffic, the official guide documents this command:
NODE_DEBUG="puppeteer:*" node path/to/script.js
For unresolved asynchronous protocol calls, inspect browser.debugInfo.pendingProtocolErrors. These errors include stacks that can help identify which code initiated the pending call. Treat verbose logs and protocol errors as potentially sensitive; keep them private and redact secrets before sharing.
6. Fix browser installation and environment problems
A Puppeteer browser executable missing error often means the browser download did not happen, the runtime is looking in a different cache, or the executable path is wrong. Check installation and environment before changing page logic.
- Check whether your package manager allowed Puppeteer’s install script to run. Some package managers can block dependency install scripts, preventing the browser download.
- Install the required browser manually if needed:
npx puppeteer browsers install. Use the equivalent command for your package manager. - Check the configured cache and executable paths against the environment where the script actually runs.
- On Puppeteer v19.0.0 and later, the default cache is
~/.cache/puppeteer. If that location is unsuitable, configurePUPPETEER_CACHE_DIRor a Puppeteer config file, then reinstall so the changed configuration takes effect. - Check platform dependencies and permissions. Linux systems and containers may lack browser libraries; Windows policies or sandbox file permissions may also prevent startup.
Platform guidance changes, so consult the troubleshooting documentation for the installed version and deployment environment. The Puppeteer documentation strongly discourages disabling the Chrome sandbox; do not use --no-sandbox as a routine debugging fix. Configure an appropriate sandbox for the environment instead.
7. Diagnose navigation, waits, frames, and interactions
Puppeteer navigation timeout
A navigation timeout does not by itself tell you whether the site is slow, the page never reached the chosen lifecycle event, or a later wait is the real bottleneck. Identify the exact awaited operation and condition. Inspect redirects, response status, and whether the page reached the state your task needs. Prefer a wait for a meaningful selector or state over increasing every timeout globally.
await page.goto('https://example.com', {waitUntil: 'domcontentloaded'});
await page.locator('[data-ready="true"]').wait();
Choose the selector based on the application’s real readiness signal. A selector that never appears will still time out; verify it in the visible page and ensure it belongs to the current frame.
Stale elements and frames
If navigation, an iframe replacement, or a rerender changes the document, reacquire the current frame and fresh element handles. Do not assume a handle obtained before that change remains usable. Confirm the target frame and element after the transition.
Click and fill failures
Check that the selector resolves to the intended element, that its type supports the action, and that it is visible and available when the action runs. A hidden duplicate or an element in another frame can make a plausible selector act on the wrong target.
Request interception
When interception is enabled, verify that every request reaches exactly one terminal handling action. A request left unresolved can stall page activity; handling one twice can fail. Reduce the interception logic to a small case and inspect the request URL in redacted form.
8. Handle timeouts and retries safely
A timeout describes what the client observed, not necessarily what the application completed. A server may process a form or other side effect even if the response is lost. Before repeating a payment, email, account creation, deletion, or other consequential action, check the application result or use its documented idempotency behavior. Do not blindly retry just because Puppeteer timed out.
For ordinary read-only work, a retry can be appropriate after checking the cause. Keep retries bounded, record each attempt, and preserve the final error. Avoid catching an error and returning empty data unless empty data is a valid, explicitly handled result.
9. Make one controlled correction
- Find the error category that matches the distinctive wording and read its explanation in documentation for your installed release.
- Reduce the script to the smallest sequence that still fails, preserving the browser configuration and page behavior that trigger it.
- Change one relevant option, path, selector, or wait condition.
- Rerun the same operation and compare the evidence with the original failure.
- Keep logging and error propagation in place until the failure is understood.
This process separates setup failures from script logic and makes it easier to tell whether a change addressed the cause or merely moved the failure.
Or skip the browser setup
If your task is to capture a website image or PDF rather than debug browser automation, ScreenshotNeo provides a screenshot API and MCP server. One GET request can return a PNG, JPEG, WebP, or PDF. See the ScreenshotNeo API 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}`);
- Cookie banners, popups, and chat widgets are removed before the shot; each cleanup step can be turned off.
- Bot checks, blank pages, timeouts, and failed loads are never billed. Responses indicate the page verdict and billing status; cache hits also cost nothing.
- An MCP server lets AI agents using Claude, Cursor, or another MCP client take screenshots, get page information, and capture PDFs.
- 1,000 screenshots per month are free with no card. Paid plans start at $5 for 3,000 shots; all features are on every plan.
Sign up free for 1,000 screenshots a month, no card required.
Troubleshooting common Puppeteer errors
| Error or symptom | Likely cause | What to check or do |
|---|---|---|
| Puppeteer browser executable missing | Install script was blocked, browser was not downloaded, or runtime cache path differs. | Run npx puppeteer browsers install; check the cache, executable configuration, and package-manager install-script policy. |
| Puppeteer launch error | Missing browser dependencies, bad executable path, permissions, sandbox configuration, or platform policy. | Enable dumpio: true, inspect process output, verify dependencies and permissions, and follow platform guidance for your version. |
| Puppeteer navigation timeout | The awaited lifecycle event or page condition did not occur before the deadline. | Identify which await timed out, inspect redirects and response, and wait for the page state the task actually requires. |
| Puppeteer protocol error | Target or session closed, browser/page lifecycle changed, or an async protocol call is still pending. | Check what was closed and when; use protocol debug output and inspect browser.debugInfo.pendingProtocolErrors. |
| Page console errors missing from terminal | Browser page output is separate from Node.js output. | Register a page.on('console', ...) listener. |
| Click or fill times out | Selector is wrong, element is hidden, frame changed, or target is not suitable for the action. | Inspect the visible page, verify element type and frame, and reacquire handles after navigation or rerender. |
| Page activity stalls with interception on | A request was not handled or was handled more than once. | Ensure every intercepted request is resolved exactly once. |
Performance, reliability, and cost notes
- Performance: Visible mode, DevTools, protocol logging, and
slowMoadd debugging overhead. Remove diagnostic settings after reproducing and fixing the issue. No speed comparison between these methods is established by the cited documentation. - Reliability: Keep errors visible to the caller, bound retries, use page-state waits that match the task, and check side effects before repeating timed-out actions.
- Cost: Puppeteer itself is JavaScript browser automation; the cited debugging guidance gives no service price or benchmark. Infrastructure cost depends on where and how you run the browser. If you only need screenshots, ScreenshotNeo offers 1,000 free shots monthly, then plans from $5 for 3,000.
- Security: Treat logs, URLs, cookies, and page output as potentially sensitive. Redact before sharing and keep the browser sandbox configured.
Frequently asked questions
How do I debug Puppeteer scripts?
Capture the complete error and stack, identify the operation that failed, and choose the debugger for that context: visible browser and page events, Node inspector, browser process logs, or protocol diagnostics.
Why does Puppeteer work locally but fail in deployment?
Compare browser download and cache paths, executable configuration, platform dependencies, permissions, sandbox setup, and runtime behavior between the two environments.
Can I debug browser-side and Node-side JavaScript with the same debugger?
They run in different contexts. Use page DevTools for code evaluated in the browser and Node’s inspector for the automation script.
Should I increase the timeout whenever navigation fails?
Only after identifying the specific awaited operation and verifying that its condition represents the page state you need. A larger timeout does not fix a missing selector, wrong frame, or blocked browser setup.
Sources
- Puppeteer debugging guide — visible browser, console forwarding, Node inspector, process output, and protocol diagnostics.
- Puppeteer troubleshooting guide — browser installation, cache, and platform issues.
- Puppeteer documentation — consult the release matching the installed version.


