TL;DR
- Puppeteer debugging is the process of tracking down failures that span two runtimes – Node.js and the browser.
- Start every Puppeteer debug session with
headless: false,slowMo, anddumpio, as they surface broken selectors, redirects, and browser crashes fast. - Split the problem by environment: use page-level tools for browser code and
node --inspect-brkfor your Node script because they fail differently. - Treat
debugger;insidepage.evaluate()as the core Puppeteer pause debug method when you need Chrome DevTools to stop inside client code. - Once you can step through code, add structured log messages, screenshots, and network request monitoring so the next failure is easier to identify than the last one.
Introduction
Puppeteer usually works, but when it doesn't, the error messages rarely tell you enough, and headless browser mode removes the one thing that would help most: being able to actually see what your browser is doing.
Puppeteer runs headless by default, and the official debugging flow centers on turning that invisible session into something you can inspect.
That gap shows up often during automation work. A page.goto() hangs in production, a selector that worked yesterday suddenly fails, or a bot check redirects your script somewhere you never expected.
In this guide, you'll learn how to debug Puppeteer with quick visual checks, browser-side pause techniques, Node.js inspection, verbose protocol logging, and a logging strategy that helps you catch issues before they turn into outages. By the end, you'll have a practical debugging hierarchy you can use on any Puppeteer script.
Why Puppeteer is uniquely difficult to debug
Puppeteer errors feel vague because your automation spans more than one runtime. Your code runs in two parts:
- Part in Node.js – the server code that launches the browser, creates a page, and calls
await page.click()orawait page.goto(). - Part inside the page itself – the client code you pass to methods like
page.evaluate().
There's also a third, less common source: the browser internals themselves.
Mostly, though, it's a multi-environment issue, instead of a single stack trace. Error messages do not always travel cleanly across those layers, which is where that split can start looking like a gulf.
A console.log inside browser code does not automatically show up in your Node terminal. A selector timeout in your Node script might actually be caused by a client-side redirect, delayed rendering, blocked network requests, or JavaScript errors inside the page. In reality, debugging Puppeteer is a question of choosing the right tool for the right environment.
Quick Puppeteer debug checks for any situation
Now that the environment split is clear, start with the cheapest signals first. These checks take almost no setup and they often expose the root cause before you ever open a dedicated DevTools for Node window.
Turn off headless mode
The fastest sanity check is to run in non-headless mode so you can literally watch the browser navigate. Puppeteer launches in headless mode by default, and switching to headless: false gives you a visible browser window that makes broken selectors, unexpected redirects, CAPTCHA walls, and login failures obvious.
Use puppeteer when launching a local browser and puppeteer-core when connecting to a remote endpoint like Browserless's hosted Puppeteer.
import puppeteer from "puppeteer";
(async () => {
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
await page.goto("https://example.com");
})();
Add slow motion with slowMo
Even a visible browser can move too fast to be useful. The slowMo option adds a delay between Puppeteer actions, which makes the execution flow readable enough to spot where things go wrong.
const browser = await puppeteer.launch({
headless: false,
slowMo: 250,
});
This option is especially helpful when your Puppeteer code clicks the wrong element, opens a new tab, or blows past a transient UI state before you can see it.
Enable dumpio for raw browser output
When the browser crashes before your script gets anywhere useful, dumpio is the next fast check. It pipes the browser process stdout and stderr to your Node process, which makes it useful when launching Puppeteer itself is failing.
const browser = await puppeteer.launch({
dumpio: true,
});
Those three checks will not solve every issue, but they quickly separate obvious visual failures from deeper logic problems. Once that first pass is done, move into browser-side debugging for the code that runs inside the page.
How to debug Puppeteer client code on the browser side
The quick checks above tell you what the browser is doing. This next round of checks tells you what the page itself is thinking. It's an especially important stage if your issue lives inside page.evaluate(), inline scripts, DOM events, or front-end state.
Capture page console output
Client-side console.log calls do not automatically appear in your Node script. You should listen for the console event on the page so you can forward those messages into your terminal as a page log.
page.on("console", (msg) => console.log("PAGE LOG:", msg.text()));
await page.evaluate(() => {
console.log(`url is ${location.href}`);
});
It's the simplest way to debug Puppeteer client code when you suspect browser-side JavaScript errors, bad selectors, or state mismatches.
Pause browser code with a debugger statement
If forwarding console output still isn't enough, pause execution inside the browser itself. Launch with devtools: true, then place debugger; inside the client code you want to inspect. Setting devtools: true forces headless to false automatically, but many developers pass both explicitly for readability.
const browser = await puppeteer.launch({ headless: false, devtools: true });
Or…
const browser = await puppeteer.launch({ devtools: true });
And then…
const page = await browser.newPage();
await page.evaluate(() => {
debugger;
const button = document.querySelector("button");
return button?.textContent;
});
It's the cleanest way to pause Puppeteer client code. Chrome DevTools will stop at the debugger statement, letting you inspect variables, step through browser code, open the console tab, and view network requests while the page is paused.
Once browser-side code is under control, the next step is the other half of the problem: debugging the Node.js script that orchestrates all of it.
How to debug Puppeteer server code on the Node.js side
Browser-side pauses usually help when the page is wrong. Server-side debugging helps when your Node script is wrong, or when you need to step over await page calls one by one and see how they affect the browser in real time.
Run the Node script with --inspect-brk
Use Node's inspector for server code. Add a debugger; statement to your test file, run the script with node --inspect-brk, then open chrome://inspect/#devices in a Chromium-based browser and attach to the process.
From there, you can resume test execution with F8 in the newly opened test browser and step through the script.
import puppeteer from "puppeteer";
(async () => {
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
debugger;
await page.goto("https://example.com");
await page.click("a");
})();
node --inspect-brk script.js
At this point, server-side debugging gets powerful. You can step over await page.click(), watch the click happen in the browser, and inspect your Node variables at the very beginning or halfway through a failing flow. If you prefer Visual Studio Code, the same inspector model works there, too.
Enable verbose protocol logging
Sometimes the problem is not your code but the conversation between Puppeteer and Chrome. Set NODE_DEBUG="puppeteer:*" to log internal DevTools protocol traffic – just be aware that these logs may include sensitive information, so filter out noisy network messages where you can.
# Unix/macOS
NODE_DEBUG="puppeteer:*" node script.js
NODE_DEBUG="puppeteer:*" node script.js 2>&1 | grep -v '"Network'
# Windows (Command Prompt)
set NODE_DEBUG=puppeteer:* && node script.js
Puppeteer 25 and later log through Node's built-in util.debuglog, which reads NODE_DEBUG. Older releases use the debug package instead, so on Puppeteer 24 and earlier set DEBUG="puppeteer:*" – that variant also accepts exclusions such as DEBUG="puppeteer:*,-puppeteer:protocol:*".
The grep filter is Unix/macOS only. On Windows, findstr /V "Network" is a rough equivalent, though output formatting may differ.
Use this tactic when a call hangs, a browser command behaves inconsistently, or generic error messages don't point to the root cause. If the protocol log confirms there's an unresolved protocol call but doesn't tell you which one, some Puppeteer versions expose a more targeted signal:
console.log(browser.debugInfo.pendingProtocolErrors);
This property returns an array of Error objects for protocol calls that never resolved. Check your version's release notes before relying on it – it isn't part of the stable public API and may not be available in older releases.
Setting up logging for Puppeteer debugging
Being able to pause and step through code solves the immediate problem. Logging is what stops the next one from being just as slow to diagnose.
Log response status and final URLs
Redirect chains, bot blocks, and auth failures often look like normal navigations until you inspect the final destination. Logging both the response status code and final URL turns silent failures into obvious ones.
const response = await page.goto("https://example.com");
console.log(`Response status: ${response?.status()}`);
console.log(`Final URL: ${response?.url() ?? page.url()}`);
page.goto() returns null for same-document navigations such as hash changes, so the optional chaining keeps the log line from throwing before you see it. A 200 on the wrong page is still a failure. A 302 to a challenge page tells you a lot more than a vague timeout.
Measure page load times
Slow pages are not always broken, but slow pages often create flaky automation. A simple timer around page.goto() gives you a baseline and helps you spot performance issues before you start blaming selectors or the default timeout.
const start = Date.now();
const response = await page.goto("https://example.com", {
waitUntil: "networkidle2",
});
const duration = Date.now() - start;
console.log(`Loaded in ${duration}ms with status ${response?.status()}`);
If one URL suddenly jumps from two seconds to twenty, you've learned something useful before touching the test code.
If pages are consistently slow, check your waitUntil option. Using networkidle0 on a site with persistent connections can cause unnecessary timeouts. The right choice depends on the site's network behavior. See our guide to waitUntil options for details. You can also read our article on fixing slow Puppeteer requests for more guidance.
Capture screenshots on failure
Headless failures are invisible by definition, so screenshots are your eyes. Capture them when the response status is bad, when a selector times out, or inside try catch blocks before you rethrow the error.
try {
const response = await page.goto("https://example.com");
if (!response || response.status() !== 200) {
await page.screenshot({ path: "error.png", fullPage: true });
}
} catch (error) {
await page.screenshot({ path: "exception.png", fullPage: true });
throw error;
}
Browserless also offers a /screenshot API for one-shot capture flows. You can add blockConsentModals=true as a query parameter to remove common cookie banners before capture.
Monitor network requests and failures
A lot of broken automations are really network problems in disguise. Monitor network requests, failed requests, and non-OK responses so you can see whether an API call, asset load, or auth request is actually causing the issue.
const safeUrl = (value) => {
const url = new URL(value);
return `${url.origin}${url.pathname}`;
};
page.on("request", (request) => {
console.log("→", request.method(), safeUrl(request.url()));
});
page.on("response", (response) => {
if (!response.ok()) {
console.log("✗", response.status(), safeUrl(response.url()));
}
});
page.on("requestfailed", (request) => {
console.log("FAILED", safeUrl(request.url()), request.failure()?.errorText);
});
You get useful log messages without enabling full protocol noise every time. The safeUrl helper drops query strings and fragments so tokens and user data never land in your logs, which matters as soon as this listener runs anywhere near production.
Use severity levels and metadata
Keep debug output for development and use warn or error in production. Whether you use winston, log4js, or plain console, make sure each log line includes:
- The action.
- The URL.
- The response status.
- Any non-sensitive metadata that helps later – timing, request IDs, or the selector you were trying to use. If you need cookie context, log allowlisted cookie names only and never their values.
Logs should help your future self, not just prove that your script ran. Once those logs exist, you'll solve many failures without a live session.
Using a live debugger for Puppeteer
Structured logs get you far, but some failures only make sense when you can watch them happen. That's where a live debugger becomes the next step after local tools.
Browserless's Live Debugger is an in-browser development environment inside the Browserless dashboard. Paste your Puppeteer script, run it against a managed browser, and watch the viewport, console output, and network activity side by side with your code.
Once a script behaves in the Live Debugger, connect the same code to Browserless's managed browsers from your own environment with the BaaS WebSocket endpoint (shown here for the shared fleet – dedicated accounts use their assigned endpoint instead):
import puppeteer from "puppeteer-core";
const browser = await puppeteer.connect({
browserWSEndpoint: "wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE",
});
If you need to watch a session that your own code started, Browserless's LiveURL streams the remote browser to a shareable URL so you can see and interact with it in real time.
When your local environment cannot reproduce the bug, or when your automation only fails in a remote browser instance, you need that visibility.
Here's Live Debugger in action:
When debugging isn't possible in real time, Browserless's Session Replay records DOM mutations, console logs, and network requests so you can play back exactly what happened after the fact. Enable it by adding replay=true to your connection URL. Session Replay is available on paid plans.
Puppeteer debugging best practices
Everything above works better when it becomes routine instead of a last-minute rescue. Keep these habits tight:
- Log from day one – Don't bolt on logging after a production failure.
- Never log sensitive information – Strip passwords, API keys, auth headers, and full URLs that contain user data.
- Wrap async flows in try/catch – Bare stack traces lose action context fast.
- Write logs for your future self – Include the page, action, selector, response status, and why the script was there.
- Store logs somewhere central – Local files stop being useful when you run hundreds of sessions a day.
- Prefer small reproducible scripts – When a big Puppeteer script is failing, reduce it to the smallest case that still breaks.
- Use the right debugger for the right layer – Browser code, Node code, and protocol issues each need different debugging methods.
Conclusion
A solid Puppeteer debug workflow is really a sequence. Start with quick checks like headless: false, slowMo, and dumpio. Move to environment-specific tools next – debugger; and Chrome DevTools for browser-side code, node --inspect-brk for the Node script, and verbose logging when the DevTools protocol itself looks suspicious. Then, add structured logging, screenshots, and network monitoring so common failures become easier to diagnose.
Audit one Puppeteer script this week using that order. If you're already running automations at scale and local reproduction is becoming the slowest part of debugging, Browserless's Live Debugger is the natural next step. Sign up to Browserless today.
Puppeteer debugging FAQs
How do I debug Puppeteer in headless mode?
You can debug Puppeteer in headless mode, but you need to add visibility in other ways since you cannot watch the browser window directly.
Start by capturing console output with page.on("console"), logging failed network requests, and saving screenshots or HTML snapshots when an error occurs.
If the issue is inside browser-side code and headless-only techniques aren't enough, add debugger; in a page.evaluate() block and relaunch with devtools: true – this forces headless to false, giving you a visible browser with DevTools open so you can step through the paused code.
For deeper problems, enable verbose logging with NODE_DEBUG="puppeteer:*" (or DEBUG="puppeteer:*" on Puppeteer 24 and earlier) to inspect DevTools protocol traffic, and log the final URL plus response status code after each navigation so you can see whether the script hit a redirect, challenge page, or silent failure.
Why does my Puppeteer script hang without an error?
A Puppeteer script usually hangs without an error because it is waiting on something that never resolves.
Common causes include:
- A selector that never appears.
- A
page.goto()waiting for the wrong load condition. - Stalled network requests.
- A popup or redirect that changed the page state.
- An async call blocked by bot detection or auth flow changes.
Start by checking the exact await page call where execution stops, then log timings around that step, monitor requestfailed and response events, and inspect browser.debugInfo.pendingProtocolErrors if the hang looks like a low-level protocol issue.
In practice, the fastest way to identify the root cause is to rerun the same Puppeteer script with headless: false and slowMo, because a hang that looks mysterious in logs is often obvious once you can see the browser state.
Can I debug Puppeteer scripts in Visual Studio Code?
Yes, you can debug Puppeteer scripts in Visual Studio Code using the built-in Node.js debugger, and it works well for server-side Puppeteer code.
Set breakpoints in your script or add a debugger; statement where you want to pause, then start the script from VS Code's Run and Debug panel using a Node launch configuration. Alternatively, run the script yourself with node --inspect-brk script.js and use a Node attach configuration to connect VS Code's debugger to the running process. Either way, you can inspect variables, step through await calls, and watch how each action affects the browser instance.
For the best experience, combine VS Code debugging with headless: false so you can step through the script in the editor while also watching the newly opened test browser respond in real time.