Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.
Attach a listener to the same Page object that performs the navigation or action, and attach it before that action runs. Pyppeteer then forwards the browser’s console.* calls to Python through the page’s console event.
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 before goto, a click, or evaluate that can produce output. Use msg.text for a readable line, msg.type to route severity, and msg.args when you need the original structured values.
What Pyppeteer is actually capturing
Browser JavaScript runs in the page context, not in the Python process. Calling console.log() therefore does not automatically write to your terminal. Pyppeteer listens to Chrome DevTools Protocol runtime events, creates JavaScript handles for the logged arguments, and emits a page-level console event. Your Python callback is the bridge between the two contexts.
Quick wins for a faster PC:
Scan for outdated or missing drivers - takes under a minuteDriver Scan →Repair Windows errors before they cause bigger problemsFix Now →Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →The event represents console API calls such as log, info, warn, error, and debug. A message object exposes three fields that matter most:
#1 Best Overall
| Field | Use it for | Important behavior |
|---|---|---|
type |
Severity filtering and routing | Identifies the console level, such as log, warning, or error. |
text |
Readable terminal and CI output | A text representation assembled from the logged arguments. |
args |
Structured diagnostics | A list of JavaScript-handle objects, including objects and values that do not have useful plain-text formatting. |
For most test logs, text is enough. If a call logs an object, array, or several values, inspect args instead of assuming that the text representation preserves every detail.
Minimal working example
Install Pyppeteer in the environment that runs your test, then create the page and register the callback before navigation:
import asyncio
from pyppeteer import launch
async def main():
browser = await launch()
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')
await page.evaluate("""
console.log('hello', 42, {foo: 'bar'});
console.warn('a warning');
console.error('an error');
""")
await browser.close()
if __name__ == '__main__':
asyncio.get_event_loop().run_until_complete(main())
A typical terminal result contains one line per browser call, with the message type in brackets. Exact object formatting can vary because text is a convenience representation rather than a serialization format.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Register the listener at the right time
- Create the browser and page.
- Attach
page.on('console', callback). - Only then call
goto, click controls, submit forms, or evaluate JavaScript.
Pages often log during their initial scripts, before the load event. Attaching after await page.goto(...) can permanently miss those messages. The same rule applies to an action that triggers a route change or a modal: install the handler first.
Make sure the listener is attached to the exact Page instance performing the work. A handler on one tab cannot receive console events from another tab.
Read structured arguments safely
msg.args contains JavaScript handles, not already-decoded Python values. For serializable values, call each handle’s asynchronous jsonValue() method. A callback supplied to the event emitter should return quickly, so schedule asynchronous inspection as a task:
import asyncio
import json
from pyppeteer import launch
async def print_arguments(msg):
values = []
for handle in msg.args:
try:
values.append(await handle.jsonValue())
except Exception:
# Functions, DOM nodes, circular objects and other remote values
# may not have a JSON representation.
try:
values.append(await handle.evaluate('(value) => String(value)'))
except Exception:
values.append('<unserializable value>')
print(f'[{msg.type}] text={msg.text}')
print('args=' + json.dumps(values, ensure_ascii=False, default=str))
def on_console(msg):
asyncio.create_task(print_arguments(msg))
async def main():
browser = await launch()
page = await browser.newPage()
page.on('console', on_console)
await page.goto('https://example.com')
await page.evaluate("console.log('user', {id: 7, roles: ['admin', 'editor']})")
await asyncio.sleep(0.1) # allow the inspection task to finish
await browser.close()
asyncio.get_event_loop().run_until_complete(main())
Do not assume every handle can be converted to JSON. Circular objects, DOM nodes, functions, and values tied to a live execution context may require explicit inspection or a string fallback. If you only need a human-readable record, print msg.text and avoid the extra protocol round trips.
PC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minuteFilter and route messages by type
Filtering in the callback keeps ordinary informational logs out of a failure report:
def on_console(msg):
if msg.type in {'error', 'warning'}:
print(f'BROWSER {msg.type.upper()}: {msg.text}')
page.on('console', on_console)
You can send errors to a test logger, warnings to a separate file, and leave ordinary logs enabled only when debugging. Keep the original msg.type rather than inferring severity from text; message wording is application-controlled.
For a complete capture, do not filter at all. A useful compromise in CI is to record every message but fail or highlight the run only when an error arrives.
Rank #3
Keep callbacks reliable in long-running tests
Do not block the event loop
The callback runs while Pyppeteer is dispatching browser events. Expensive file operations, network requests, or large synchronous formatting can delay other automation. Push heavier work into an asyncio task or a queue and let the callback perform only timestamping and enqueueing.
Do these 3 things before closing this tab:
1Fix the driver behind crashes, sound loss and screen glitches2Clear out junk files and repair common Windows errors3Scan for outdated or missing drivers - takes under a minuteClose handles when you retain them
Handles in msg.args refer to objects in the browser’s execution context. Decode values immediately when possible instead of storing handles for the whole test suite. If you must retain a handle for later inspection, release it after use so repeated logging does not accumulate remote objects.
Remove a temporary listener
For a one-off diagnostic, keep a reference to the callback and remove it when the operation ends:
def on_console(msg):
print(msg.text)
page.on('console', on_console)
try:
await page.click('#run-check')
finally:
page.removeListener('console', on_console)
Leave the listener installed for the lifetime of a page when you are collecting a full navigation or test transcript.
Console messages from workers are a separate case
Pyppeteer’s normal page-console path handles page runtime events and log entries, but its implementation excludes log entries whose source is worker. Consequently, output from a service worker, dedicated worker, or another worker target may not appear in page.on('console', ...).
Recommended Free Tools
If a page appears silent while the application clearly logs from a worker, investigate the worker target and its lifecycle separately. Confirm whether the worker exists, when it starts, and which target owns the message; do not conclude that the page listener is broken simply because worker output is absent.
Diagnose missing or confusing output
The terminal is empty
- Confirm that the callback is attached to the same page used by
gotoorevaluate. - Move registration before the triggering operation.
- Verify that the JavaScript actually reaches the
console.*call; a navigation failure can prevent it. - Remember that browser-side output never appears in Python automatically; the event listener is required.
Only part of an object appears
Use msg.args and call jsonValue() for each serializable handle. msg.text is intentionally a line-oriented representation and is not a lossless object serializer.
Messages appear duplicated
Check that setup code is not adding a new listener every time a test runs on the same page. Keep one callback per page, or remove the previous callback before installing a replacement.
A callback causes slow or flaky tests
Reduce work inside the event callback, enqueue records for later processing, and avoid repeated conversion of large objects. Logging every argument is useful during diagnosis but can be expensive on pages that emit high-volume telemetry.
The Tool Desk
Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Behavior differs between machines
Check the installed Pyppeteer package and the Chromium revision it launches. Differences in browser versions, page timing, or application bundles can change which messages are emitted and when. Capture the versions alongside your test logs so a failure can be reproduced.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Test the capture path independently
Before debugging a complex application, prove that the bridge works with a controlled page:
await page.evaluate("""
console.log('plain', 1);
console.warn('warning');
console.error('error', {code: 500});
""")
If these three calls reach Python, Pyppeteer event delivery is functioning and the remaining problem is in page timing, conditional application code, or a worker context. If none arrive, inspect listener placement and page identity first.
Or skip the browser setup
If your goal is a rendered image or PDF rather than a stream of browser console records, ScreenshotNeo provides a single HTTP request. It does not replace Pyppeteer’s console event—use Pyppeteer when you need JavaScript logs—but it can produce a clean visual artifact after you identify the page state.
For the full parameter list, see the ScreenshotNeo API documentation.
cURL
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 accepts the cookie or consent banner like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each cleanup step can be disabled. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and whether the request was billed. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients.
The Free plan includes 1,000 screenshots per month without a card. Paid plans start at $5 for 3,000 shots, and every feature is available on every plan. Create a free ScreenshotNeo account to try it.
Performance, reliability, and cost considerations
- Timing: early registration captures startup logs; late registration is cheaper only if those logs are irrelevant.
- Volume: text-only callbacks have low overhead, while decoding many large argument objects adds DevTools round trips.
- Retention: write or forward records incrementally instead of keeping an unbounded in-memory list.
- Isolation: use a separate listener and output context for each page when tests run concurrently.
- Interpretation: a console error is evidence of a logged error, not proof that a request failed or that the test should fail. Combine it with your test’s assertions and network diagnostics.
Frequently Asked Questions
Can I capture console output from a page after it has already loaded?
Yes. Attach the listener to the existing page and then trigger another action or evaluate JavaScript. Messages emitted before the listener was attached cannot be recovered from that page event stream.
Free tools Windows power users keep installed
One-click scans. No signup required.
Does msg.text preserve the original JavaScript object?
No. It is a convenient text representation. Use the handles in msg.args and jsonValue() when you need serializable structure.
Why is a worker’s console.log missing from my page handler?
Pyppeteer’s page implementation does not emit log entries whose source is worker through the normal page-console path. Inspect the worker target and lifecycle separately.
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.

