ScreenshotNeo

BlogHow-to

How to Capture Console Messages in Pyppeteer

Capture browser console.log, errors, warnings, arguments, and worker diagnostics in Pyppeteer with reliable Python examples and troubleshooting.

By the ScreenshotNeo team1 October 20265 min read

Attach a handler to the page’s console event before navigation or any action that can log. Pyppeteer then forwards browser-side messages to Python, where you can read msg.type, msg.text, and msg.args.

import asyncio
from pyppeteer import launch

async def main():
    browser = await launch()
    page = await browser.newPage()

    page.on('console', lambda msg: print(f'[{msg.type}] {msg.text}'))

    await page.goto('https://example.com')
    await page.evaluate("console.log('hello', 42, {foo: 'bar'})")

    await browser.close()

asyncio.get_event_loop().run_until_complete(main())

The listener must be registered on the same Page instance that performs the navigation, click, or evaluation. Browser console output runs in the page context; it does not automatically appear in the Python process.

1. How the console event works

Pyppeteer listens to Chrome DevTools Protocol runtime events and emits a page console event for each browser console call. A ConsoleMessage contains:

Property Use it for
type Classifying messages such as log, error, and warning.
text A convenient line-oriented representation for terminal output and CI logs.
args The original JavaScript argument handles, useful when multiple arguments or structured objects matter.

Primitive arguments are joined into text. The original values remain available through args as JavaScript handles, so object inspection requires explicit conversion or property inspection.

2. A complete runnable example

This script captures messages during page load and during an explicit evaluation, prints their type and text, and closes the browser even when the page reports several message types.

import asyncio
from pyppeteer import launch

async def main():
    browser = await launch(headless=True)
    page = await browser.newPage()

    def on_console(msg):
        print(f'[{msg.type}] {msg.text}')

    page.on('console', on_console)

    await page.goto('https://example.com', {'waitUntil': 'networkidle2'})
    await page.evaluate("""
        console.log('page loaded', {section: 'demo'});
        console.warn('sample warning');
        console.error('sample error');
    """)

    await browser.close()

asyncio.get_event_loop().run_until_complete(main())

Register the handler before goto. Pages can log during their earliest scripts, so attaching it afterward can miss those messages.

3. Read structured arguments with msg.args

Use msg.text for simple output. Use msg.args when you need the separate values passed to console.log, such as an object and an identifier.

import asyncio
from pyppeteer import launch

async def main():
    browser = await launch()
    page = await browser.newPage()

    async def on_console(msg):
        print('type:', msg.type)
        print('text:', msg.text)
        for index, handle in enumerate(msg.args):
            try:
                value = await handle.jsonValue()
                print(f'arg {index}:', value)
            except Exception:
                # Some browser values are not JSON serializable.
                print(f'arg {index}:', await handle.toString())

    page.on('console', on_console)
    await page.evaluate("console.log('user', {id: 7, active: true})")
    await browser.close()

asyncio.get_event_loop().run_until_complete(main())

jsonValue() is appropriate for serializable values. Functions, DOM nodes, symbols, and other non-serializable handles need string conversion or targeted property inspection. Keep in mind that the console callback may be asynchronous when you inspect handles.

4. Filter errors and warnings

Route only actionable levels to your test output while retaining the full listener for debugging.

def on_console(msg):
    if msg.type in {'error', 'warning'}:
        print(f'BROWSER {msg.type.upper()}: {msg.text}')

page.on('console', on_console)

Filtering on type avoids noisy informational logs. Do not filter during an initial diagnosis if you are unsure which level a site uses.

5. Listener timing and page lifecycle

  1. Create the page.
  2. Attach the console handler.
  3. Navigate, click, submit forms, or call evaluate.
  4. Remove the handler or close the page when the diagnostic run ends.

The handler remains active until it is removed or the page closes. If your code opens a new tab or popup, attach a handler to that new Page as well; a listener on the original page does not automatically observe every page in the browser.

6. Worker and service-worker logging

Worker-originated logging needs separate investigation. Pyppeteer’s page implementation handles log entries but does not emit a normal page-console message when the log source is a worker. If a message appears in DevTools but not in your callback, check whether it came from a dedicated worker or service worker and inspect that target’s lifecycle and events separately.

7. Troubleshooting

Symptom Cause Fix
No output at all The handler is attached to a different page or after the operation. Attach it immediately after newPage(), before goto, clicks, or evaluate.
evaluate logs are missing JavaScript runs in the browser context, not the Python process. Use page.on('console', ...); do not expect Python’s stdout to receive browser logs automatically.
Only part of a log appears msg.text is a flattened representation. Iterate over msg.args and call jsonValue() or inspect handles explicitly.
Worker messages are absent Worker log entries are excluded from the normal page-console path. Identify the worker target and monitor its lifecycle separately.
Callback errors while inspecting values An argument is not JSON serializable or its handle is no longer valid. Use guarded conversion, toString(), or inspect only properties you need while the page is alive.
Behavior differs between machines Installed Pyppeteer and Chromium versions differ. Record both versions and reproduce with the same pair before changing the listener.

8. Reliability and performance notes

  • Attach one long-lived listener per page instead of repeatedly registering callbacks around individual actions.
  • Keep callbacks lightweight. Write raw messages to a queue if processing, serialization, or network logging is expensive.
  • Use msg.type filtering in CI when only errors and warnings matter.
  • Close or dispose argument handles after deep inspection when your workflow creates many messages.
  • Capture the URL, action, message type, and text together so failures can be reproduced.
  • For deterministic diagnostics, attach listeners before navigation and use an explicit navigation wait condition.

9. Or skip the browser setup

If your goal is a clean screenshot rather than browser-console diagnostics, ScreenshotNeo provides a GET endpoint that returns PNG, JPEG, WebP, or PDF. See the ScreenshotNeo API documentation for parameters and response details.

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 and consent banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status. ScreenshotNeo also provides an MCP server so Claude, Cursor, and other MCP clients can take screenshots. The free plan includes 1,000 screenshots per month without a card; paid plans start at $5 for 3,000 screenshots.

Create a free ScreenshotNeo account.

10. FAQ

Can I capture only errors?

Yes. Check msg.type and print or store only error and warning messages.

Why use args instead of text?

text is convenient for lines of output; args preserves separate values and structured data.

Does the listener capture console output from every tab?

No. Attach a listener to each Page whose messages you need.

Will it always capture service-worker logs?

No. Worker-originated entries may not be emitted through the normal page-console event, so inspect worker targets separately.

When should I remove the handler?

Leave it installed for the diagnostic lifetime of the page, then remove it or close the page to release resources.