What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

In a Node.js script that uses Puppeteer, attach listeners before navigation or any interaction that can fail. Use the console event for messages emitted by console.*, pageerror for uncaught exceptions in page JavaScript, error for a page crash, and requestfailed for requests that fail at the network level. Keep those signals separate, record them with the URL and timestamp, and persist them in your test or CI logs.

The minimal Puppeteer solution

Install Puppeteer in a Node.js project, create a page, register every listener, and only then navigate:

npm install puppeteer
const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({headless: true});
  const page = await browser.newPage();

  page.on('console', msg => {
    console.log(`[browser console:${msg.type()}] ${msg.text()}`);
  });

  page.on('pageerror', error => {
    console.error('[uncaught page exception]', error.name, error.message);
    if (error.stack) console.error(error.stack);
  });

  page.on('error', error => {
    console.error('[page crashed]', error.name, error.message);
  });

  page.on('requestfailed', request => {
    console.error('[request failed]', request.method(), request.url(), request.failure());
  });

  await page.goto('https://example.com', {waitUntil: 'networkidle2'});
  await page.close();
  await browser.close();
})();

The listener must exist before goto(), clicks, form submissions, or scripts run. An event emitted before a listener is attached cannot be recovered. The console callback receives a Puppeteer console-message object; msg.type() identifies values such as error, warning, log, and info, while msg.text() gives a readable representation.

What each event actually tells you

Event Meaning Typical use
console A page called a console API, including console.error(), warnings, logs, and informational output. Puppeteer can also surface page errors or warnings through this stream. Collect application diagnostics and deliberately logged errors.
pageerror An uncaught exception escaped page JavaScript. Find failures that were thrown without a preceding console.error().
error The page crashed. Detect a browser renderer or page crash, which is different from a JavaScript exception.
requestfailed A request failed at the network or transport layer. Identify DNS, connection, TLS, or aborted-resource failures.

An HTTP 404 or 503 still produces an HTTP response, so it does not emit requestfailed. If HTTP status is part of your test, inspect the response returned by page.goto() or listen for responses separately. Do not treat a network failure, an HTTP error response, a page crash, and an uncaught exception as interchangeable evidence.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Capture structured records for CI and test reports

Printing a line is useful while debugging, but structured records are easier to archive, filter, and attach to a failed build. This example writes JSON Lines to standard output and preserves the event category, page URL, message, and stack when available.

const puppeteer = require('puppeteer');

function write(record) {
  process.stdout.write(JSON.stringify({
    time: new Date().toISOString(),
    ...record
  }) + 'n');
}

(async () => {
  const browser = await puppeteer.launch({headless: true});
  const page = await browser.newPage();

  page.on('console', msg => {
    write({
      kind: 'console',
      level: msg.type(),
      url: page.url(),
      text: msg.text()
    });
  });

  page.on('pageerror', error => {
    write({
      kind: 'pageerror',
      url: page.url(),
      name: error?.name,
      message: error?.message ?? String(error),
      stack: error?.stack ?? null
    });
  });

  page.on('error', error => {
    write({
      kind: 'page-crash',
      url: page.url(),
      name: error?.name,
      message: error?.message ?? String(error),
      stack: error?.stack ?? null
    });
  });

  page.on('requestfailed', request => {
    write({
      kind: 'requestfailed',
      url: page.url(),
      requestUrl: request.url(),
      method: request.method(),
      failure: request.failure()
    });
  });

  const response = await page.goto(process.env.TEST_URL || 'https://example.com', {
    waitUntil: 'networkidle2',
    timeout: 30000
  });
  write({kind: 'navigation', status: response?.status() ?? null, url: page.url()});

  await browser.close();
})().catch(error => {
  write({kind: 'runner-error', name: error.name, message: error.message, stack: error.stack});
  process.exitCode = 1;
});

Keep the original kind. Filtering only for console.error misses exceptions that are thrown directly, while recording every console message as a defect creates noise. A practical CI policy is to archive all records, fail a test on pageerror (if that is your application contract), and use a separate allowlist for known third-party warnings.

Exception payloads can vary with the Puppeteer version and execution context. Preserve the available name, message, and stack rather than assuming the value is always a native Error. The optional chaining and fallback above prevent the logger itself from crashing while handling an unusual payload.

Make navigation and interactions observable

Attach listeners once per page

Register listeners immediately after newPage(). If your test creates several pages or popups, attach the same handlers to each page, or centralize this in a helper:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
function observePage(page, write) {
  page.on('console', msg => write({kind: 'console', level: msg.type(), text: msg.text(), url: page.url()}));
  page.on('pageerror', error => write({kind: 'pageerror', message: error?.message ?? String(error), stack: error?.stack ?? null, url: page.url()}));
  page.on('error', error => write({kind: 'page-crash', message: error?.message ?? String(error), url: page.url()}));
  page.on('requestfailed', request => write({kind: 'requestfailed', requestUrl: request.url(), failure: request.failure(), url: page.url()}));
}

Cover popups and new tabs

A listener on the original page does not automatically observe a newly opened tab. Listen for the browser’s targetcreated event, convert the target to a page, and install the handlers before driving it:

browser.on('targetcreated', async target => {
  if (target.type() === 'page') {
    const popup = await target.page();
    observePage(popup, write);
  }
});

The target callback is asynchronous, so coordinate it with your test’s popup wait when the first script in the new tab must not be missed. In many tests, Promise.all([browser.waitForTarget(...), click]) followed by immediate observation is clearer than relying on timing.

Exercise the failing code after instrumentation

Navigate, wait for the required selector, then perform the click or form submission. Set a suitable timeout and catch runner-level failures separately; a timeout from Puppeteer is not itself a page JavaScript exception.

await page.goto('https://example.com/app', {waitUntil: 'domcontentloaded'});
await page.waitForSelector('#save', {timeout: 10000});
await page.click('#save');
await page.waitForTimeout(500);

Use Chrome DevTools for interactive diagnosis

When you can reproduce the problem manually, open the DevTools Console. It displays error and warning stack traces and lets you filter by severity, script URL, and the selected JavaScript execution context. Enable “Preserve log” before reloading when the failure disappears during navigation. These controls are particularly useful for separating your bundle’s messages from extensions, analytics, and third-party frames.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

DevTools is an inspection tool, not a replacement for automation listeners: it helps a person reproduce and narrow a problem, whereas Puppeteer forwarding gives CI or a batch job durable records.

Playwright and lower-level CDP choices

If the project already uses Playwright

Use Playwright’s page events instead of adding Puppeteer solely for logging. The concepts remain the same: listen for console messages, page errors, crashes, and failed requests before navigation. Keeping one automation framework avoids duplicated browser lifecycle and timeout behavior.

If you need Chrome DevTools Protocol events

CDP exposes lower-level Runtime console API and Log event surfaces. The legacy CDP Console domain is deprecated in favor of Runtime or Log, so choose the current domain supported by the Chromium version you operate. Raw CDP is appropriate when you need protocol-level integration or are attaching to a browser owned by another process; it requires more lifecycle and payload handling than a framework page listener.

Attaching Playwright to an existing Chromium

Playwright’s chromium.connectOverCDP() can attach to an existing Chromium instance. The documented limitations are important: CDP attachment is Chromium-only and has significantly lower fidelity than Playwright’s normal protocol connection. If you control both browser ends and need advanced Playwright behavior, prefer the standard Playwright connection.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Common failures and precise fixes

  • No browser messages appear: the listener was attached after goto() or after the interaction. Move registration immediately after newPage().
  • Only console.error is recorded: an uncaught exception may never call the console API. Add pageerror.
  • Too many records: the console event includes normal logs and warnings. Store all levels but filter reports by msg.type() and event kind.
  • A 404 is missing from request failures: that is expected. A 404 or 503 is an HTTP response; inspect response status separately.
  • The browser process exits with no page error: distinguish a runner or browser-process failure from the page’s error crash event and retain the outer catch record.
  • Stack is absent: preserve the message and name anyway. Minified production bundles, cross-origin frames, and protocol payload differences can limit stack detail.
  • Popup errors are absent: install listeners on every newly created page, not only the first page.
  • Logs from an iframe are confusing: include the page URL and, where your framework exposes it, the execution context or frame identity so messages can be attributed correctly.
  • CI hangs: set navigation and action timeouts, close the browser in a finally block, and keep logging independent of the assertion that may fail.

Performance, reliability, and data handling

Event listeners are lightweight, but logging every console message can become expensive on chatty applications. Prefer JSON Lines or another streaming format, cap message size, and send records to the test artifact rather than repeatedly rewriting one large file. Redact tokens, cookies, authorization values, and personal data before exporting logs. Do not evaluate arbitrary page objects merely to stringify them; msg.text() is safer for a basic record, while deeper remote-object inspection should be deliberate.

Use a stable browser version in CI and record the Puppeteer and Chromium versions with each run. This makes changes in exception serialization or console payloads explainable. Always close pages and browsers in cleanup code so a crashed renderer does not leave orphaned processes.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If your goal is a visual capture rather than debugging JavaScript behavior, ScreenshotNeo provides a single HTTP request that returns a PNG, JPEG, WebP, or PDF. It accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be disabled. Only clean shots are billed, while bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. Its MCP server offers take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.

See the parameter reference in the ScreenshotNeo documentation. A direct call is:

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

The same request in 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)

And in 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 full-page and selector captures, device and viewport controls, retina scale, dark mode, PDF options, custom CSS and JavaScript, click and wait conditions, request blocking, headers and cookies, timezone and geolocation, transparent backgrounds, resizing, chosen cache TTLs, signed links, asynchronous jobs with signed webhooks, bulk capture for up to 100 URLs per call, a usage API, and an OpenAPI specification. Every feature is on every plan: 1,000 shots per month free with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

FAQ

Does console.error() catch every JavaScript error?

No. It captures an explicit console call. An exception can be thrown without any console call, so use pageerror for uncaught page exceptions.

Should a failed HTTP response be labeled a network failure?

No. A 404 or 503 is still an HTTP response. Record its status separately from requestfailed, which represents a request that failed before receiving a response.

When should I choose raw CDP?

Choose it when you need protocol-level Runtime or Log events or must attach to a browser managed elsewhere. For ordinary Puppeteer or Playwright tests, framework events require less plumbing.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Frequently Asked Questions

Can I capture errors from a page opened by a click?

Yes. Observe newly created targets or popups and attach the same listeners to the resulting Page before interacting with it.

Why is my error stack sometimes missing?

Stack availability varies with bundling, cross-origin execution contexts, and Puppeteer payloads. Always retain the event kind, name, and message as a fallback.

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.