QA How-To
Selenium browser console logs in Python (2026)
Capture selenium browser console logs python tests emit with WebDriver BiDi or Chromium logs, then filter errors, wait reliably, and attach safe CI evidence.
23 min read | 2,807 words
TL;DR
For modern event-driven capture, set options.enable_bidi = True and register driver.script.add_console_message_handler(entries.append). For existing Chromium suites, request goog:loggingPrefs and call driver.get_log('browser'), knowing that reads drain the available buffer.
Key Takeaways
- Enable options.enable_bidi before driver creation to use Selenium's live console and JavaScript error handlers.
- Register a console callback before the action or navigation that can emit the message.
- Use add_javascript_error_handler for uncaught script errors, not as an alias for console.error calls.
- For Chromium get_log('browser'), remember that each read returns and clears the currently available entries.
- Wait on a specific captured condition instead of sleeping for an arbitrary duration.
- Keep collections per test and sanitize console text before saving CI artifacts.
A practical selenium browser console logs python setup lets a test see front-end errors that never become visible DOM text. Selenium's current Python binding provides high-level WebDriver BiDi callbacks for console messages and JavaScript errors, plus the familiar Chromium get_log('browser') route for buffered messages.
The code is short, but reliable use requires clear timing and ownership. A listener must be installed before the event, a buffered read must not be consumed by two helpers, and a failure rule must separate product defects from third-party noise. This guide builds both workflows, integrates them with pytest, and shows how to preserve useful evidence without turning every warning into a flaky test.
TL;DR
| Requirement | Python API | Key behavior |
|---|---|---|
| Live console messages | driver.script.add_console_message_handler(callback) |
Requires a BiDi-enabled session |
| Live uncaught JavaScript errors | add_javascript_error_handler(callback) |
Produces JavaScript error entries |
| Buffered Chromium console logs | driver.get_log('browser') |
A read drains available entries |
| Stop a live callback | remove_console_message_handler(handler_id) |
Store the ID returned at registration |
| Reliable assertion | WebDriverWait(...).until(...) |
Wait for a specific message or product state |
| Parallel safety | One collection per driver fixture | Never use a module-level shared list |
1. Selenium browser console logs python concepts
Browser console output is page evidence, not Selenium's own debug logging. A page can emit console.debug, console.info, console.log, console.warn, or console.error. The browser can also report resource failures, policy warnings, and uncaught JavaScript exceptions. Selenium client logging, browser driver logs, and Grid logs describe different layers and should be collected separately.
The distinction between console calls and JavaScript errors is important. console.error('invalid state') records an application-selected message but does not throw. throw new Error('invalid state') interrupts script execution unless the error is caught. Selenium's BiDi API reflects this distinction with separate console-message and JavaScript-error handlers. A mature test policy can observe both without pretending they mean the same thing.
Console evidence is strongest when it supports a user-level assertion. For example, a test can assert that a dashboard remains blank and attach a matching uncaught error from the application bundle. A test that only asserts an empty console might pass even though the feature did nothing. If the visible assertion is failing because synchronization is weak, fix that first using the patterns in Selenium NoSuchElementException troubleshooting.
2. Install Selenium and request a BiDi session
Use a current Selenium 4 package and pin it in the environment definition that CI installs. Selenium Manager normally resolves a local driver when a supported browser is present. The examples below use Chrome, but the BiDi API is designed around a browser standard. Support for a specific handler still needs to be verified on every browser and remote image in your matrix.
selenium==4.46.0
pytest>=8.0
Enable BiDi before creating the driver. The option asks the remote end to return the WebSocket URL used for events.
from selenium import webdriver
options = webdriver.ChromeOptions()
options.enable_bidi = True
driver = webdriver.Chrome(options=options)
try:
assert isinstance(driver.capabilities.get('webSocketUrl'), str)
finally:
driver.quit()
If driver.script raises an error about a missing URL, inspect the returned capabilities and confirm that enable_bidi was set before session creation. On Grid, also verify the Selenium server, node, browser, and driver combination. Client methods cannot create server-side support that the remote environment does not implement.
Do not add goog:loggingPrefs just to make BiDi work. That is a separate Chromium logging capability for the pull-based route. Keeping the configurations separate makes test behavior and browser support easier to explain.
3. Capture live console messages with WebDriver BiDi
The high-level console API accepts a Python callable. Selenium invokes it with a typed console log entry, and the registration method returns an ID used for removal. The following standalone program registers before navigation, emits a page message, waits for it, and removes the callback.
from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
options = webdriver.ChromeOptions()
options.enable_bidi = True
driver = webdriver.Chrome(options=options)
entries = []
handler_id = driver.script.add_console_message_handler(entries.append)
try:
driver.get('data:text/html,<title>Console test</title>')
driver.execute_script(
"console.error('inventory refresh failed');"
)
WebDriverWait(driver, 5).until(
lambda _driver: any(
'inventory refresh failed' in entry.text
for entry in entries
)
)
for entry in entries:
print(entry.level, entry.method, entry.text, entry.timestamp)
finally:
driver.script.remove_console_message_handler(handler_id)
driver.quit()
The entry exposes fields such as level, text, timestamp, method, args, and type_. Prefer those fields over parsing a formatted message. Keep the callback fast. Appending to a per-test collection is appropriate, while screenshots, network calls, or complex assertion logic inside the callback can delay event processing and complicate cleanup.
Register before the event of interest. A callback added after page load cannot reconstruct console output that was already emitted during bootstrap.
4. Capture uncaught JavaScript errors separately
Use the JavaScript error handler when the test contract concerns runtime exceptions. It returns a typed entry with level, text, timestamp, stack-trace data, and type. This makes it possible to enforce a rule such as no uncaught application exception during account creation without treating an intentional console.warn as equivalent.
from selenium.webdriver.support.ui import WebDriverWait
errors = []
error_id = driver.script.add_javascript_error_handler(errors.append)
try:
driver.get('data:text/html,<title>Error test</title>')
driver.execute_script(
"setTimeout(() => { throw new Error('render failed'); }, 0);"
)
WebDriverWait(driver, 5).until(lambda _driver: len(errors) > 0)
assert 'render failed' in errors[0].text
print(errors[0].stacktrace)
finally:
driver.script.remove_javascript_error_handler(error_id)
A caught exception might not appear as an uncaught JavaScript error because application code handled it. That is correct behavior for this signal. If the application converts caught errors into a console message, telemetry event, or error component, assert the appropriate output instead.
Keep console and JavaScript error collections separate even when both are enabled. A test report that says uncaught JavaScript exception is more actionable than a generic console failure. Separate collections also let a negative scenario allow one expected console error while still rejecting unexpected uncaught exceptions.
5. Selenium browser console logs python with get_log
Existing Chrome and Edge frameworks often use the pull-based browser log. Configure the Chromium logging preference before driver creation, then call get_log('browser'). Python returns a list of dictionaries, commonly containing level, message, source, and timestamp.
from selenium import webdriver
options = webdriver.ChromeOptions()
options.set_capability(
'goog:loggingPrefs',
{'browser': 'ALL'},
)
driver = webdriver.Chrome(options=options)
try:
driver.get('data:text/html,<title>Buffered log test</title>')
driver.execute_script(
"console.warn('deprecated checkout path');"
)
entries = driver.get_log('browser')
for entry in entries:
print(
entry.get('level'),
entry.get('source'),
entry.get('timestamp'),
entry.get('message'),
)
assert any(
'deprecated checkout path' in entry.get('message', '')
for entry in entries
)
finally:
driver.quit()
This capability is Chromium-specific, even though the Python WebDriver object exposes get_log. Querying driver.log_types can help an environment check confirm which types the remote end offers. Do not market the route as portable Firefox logging without proving it against the actual driver.
Each read consumes the currently available entries. Capture once, store the returned list, and share it with the assertion and reporter.
6. Define capture windows for asynchronous pages
A console read made immediately after a click can beat the promise or timer that emits the error. Sleeping for two seconds only hides the race on fast machines and wastes time on slow ones. Use an observable completion condition, then collect or assert on the messages.
With BiDi, wait directly for a specific entry when that event is the expected outcome. With buffered logs, first wait for the UI state that signals the feature finished, then call get_log. If polling the log itself, remember that each poll drains entries and must accumulate them in one owner.
def wait_for_buffered_message(driver, text, timeout=5):
collected = []
def found(_driver):
collected.extend(driver.get_log('browser'))
return any(
text in item.get('message', '')
for item in collected
)
WebDriverWait(driver, timeout).until(found)
return collected
The helper retains messages across polls so an early nonmatching entry is not lost from final diagnostics. Use it only when a buffered message is itself the expected signal. For normal feature tests, waiting on the product state is clearer.
Mark capture boundaries explicitly: clear known startup noise, perform one action, wait for completion, and evaluate that window. This makes failures reproducible and prevents a login warning from being blamed on a later checkout test step.
7. Build a pytest fixture that owns cleanup
A function-scoped fixture can create a BiDi-enabled driver, register collectors, expose them to the test, and remove handlers even when an assertion fails. A small data object is clearer than returning an unlabeled tuple.
from dataclasses import dataclass
import pytest
from selenium import webdriver
@dataclass
class BrowserEvidence:
driver: webdriver.Chrome
console: list
javascript_errors: list
@pytest.fixture
def browser_evidence():
options = webdriver.ChromeOptions()
options.enable_bidi = True
driver = webdriver.Chrome(options=options)
console = []
javascript_errors = []
console_id = driver.script.add_console_message_handler(console.append)
error_id = driver.script.add_javascript_error_handler(
javascript_errors.append
)
try:
yield BrowserEvidence(driver, console, javascript_errors)
finally:
driver.script.remove_console_message_handler(console_id)
driver.script.remove_javascript_error_handler(error_id)
driver.quit()
Keep the fixture function-scoped unless a carefully designed driver pool resets all browser and handler state. A session-scoped collector grows without bounds and makes attribution difficult. Module-level lists are worse because parallel workers or multiple drivers can mix evidence.
If a test needs messages emitted during initial navigation, the fixture is ideal because handlers exist before the test calls get. If driver creation itself navigates through an application fixture, register immediately after session creation and before that setup navigation.
8. Turn raw messages into a maintainable test policy
A good console policy asks whether the message represents a product risk owned by the team. It does not fail because a string happens to contain error. Start with structured severity, then evaluate source, test phase, application origin, and a precise allowlist.
For BiDi entries, use typed fields. For get_log dictionaries, normalize keys into your own dataclass before policy evaluation. Unit test the policy with plain Python objects so changes do not require a browser. A policy function might return violations rather than raising immediately, allowing the test reporter to show every relevant entry in one concise failure.
Allowlist entries should contain a stable signature, an owning component, an issue reference, and preferably an expiry. Never suppress all Failed to load resource messages. That phrase can cover anything from a blocked analytics pixel to the missing API call that broke the page.
Run a new policy in observation mode first. Categorize existing messages, fix application-owned exceptions, and establish a narrow baseline. Then enforce it on critical flows. This yields a trusted signal instead of a permanently ignored red build. For ideas on assisted code generation without surrendering review, see using Copilot to write Selenium tests.
9. Save secure, useful failure artifacts
Console output can contain access tokens, signed URLs, customer identifiers, GraphQL variables, and serialized state. Treat it as potentially sensitive. Redact known token patterns and URL query values before writing artifacts, and follow the same access and retention controls used for screenshots and videos.
A useful per-test artifact contains the test node ID, phase, browser name and version, timestamp, structured level, sanitized text, and relevant stack frames. Do not create one suite-wide log that parallel workers overwrite. Use pytest's node ID plus a safe unique suffix for the artifact path.
Preserve the original assertion failure. If artifact writing or handler removal raises during teardown, record that as secondary diagnostic information and still attempt driver.quit(). A console helper should never replace expected order confirmation, but none appeared with a less useful file-system exception.
In CI, attach evidence only to the related test result and consider truncating very large captures after preserving a count and the relevant tail. High message volume can itself be a performance or logging defect, but a multi-megabyte attachment rarely helps the first diagnosis. The Jenkins Selenium pipeline guide provides the surrounding artifact and parallel-execution context.
10. Compare BiDi handlers and Chromium get_log
The best choice depends on the suite, browser matrix, and assertion goal.
| Criterion | WebDriver BiDi handlers | Chromium get_log('browser') |
|---|---|---|
| Delivery model | Push callback | Pull buffered entries |
| Setup | options.enable_bidi = True |
goog:loggingPrefs capability |
| Data shape | Typed Python entry objects | Dictionaries with driver-formatted messages |
| Timing | Live while the handler is installed | Available when the buffer is read |
| Cleanup | Remove handler by returned ID | Reads drain returned entries |
| Browser direction | Standards-oriented | Chromium-specific in practical use |
| Best use | New event-driven console and error checks | Existing Chromium diagnostics and simple capture |
Prefer BiDi for new code when its public high-level feature covers the scenario and your browsers implement it. Keep get_log when an established Chromium framework already has reliable capture and the portability cost is acceptable. A small adapter can hide the collection mechanism while exposing one normalized evidence model to tests.
Avoid building new code around deprecated asynchronous CDP log helpers when the high-level driver.script API provides the needed event. This reduces protocol coupling and removes event-loop complexity from normal pytest code. Compare language-specific design choices in Selenium browser console logs in Java.
Interview Questions and Answers
Q: How do you capture console logs in Selenium Python in 2026?
For live events, enable BiDi on the browser options and register driver.script.add_console_message_handler. For an existing Chromium suite, request goog:loggingPrefs and call driver.get_log('browser'). I would explain the push versus pull behavior and verify browser support. I would also store the returned handler ID, keep the callback lightweight, wait on a specific entry, and remove the handler before quitting. Those lifecycle details distinguish runnable framework code from a one-line API answer.
Q: Why does a second get_log call not include the first messages?
The buffered read returns and clears the entries currently available for that log type. If one helper reads before another, the second cannot see those same entries. The framework should give one collector ownership and share its result. If polling is required, that collector accumulates every drained batch until the bounded condition succeeds. Reporters receive the saved collection and never call get_log independently.
Q: What is the difference between add_console_message_handler and add_javascript_error_handler?
The console handler receives explicit console API messages. The JavaScript error handler receives script error entries, including uncaught exceptions. Keeping them separate supports different failure policies and clearer reporting. A negative test may legitimately emit console.error while an uncaught exception still indicates a broken runtime path. Separate typed collections preserve that distinction.
Q: How do you wait for an asynchronous console message?
Install the handler before the action, collect entries, and use WebDriverWait with a predicate for the specific text or structured property. For buffered logs, accumulate entries across bounded polls or wait for a product completion state before one read. I avoid fixed sleeps. When the console entry is only diagnostic, I wait for the customer-visible completion state first and then evaluate collected evidence. That keeps synchronization tied to product behavior instead of incidental logging timing.
Q: Should console errors replace UI assertions?
No. Console evidence is usually diagnostic or an additional engineering contract. The test should still prove the customer-visible result, because a quiet console does not mean the feature succeeded and a noisy third party does not necessarily mean it failed. I use console output to explain a failed state or enforce a narrow engineering contract, then attach it beside the screenshot and primary assertion. This keeps the test outcome meaningful to product and engineering teams.
Q: How do you handle console logs in parallel pytest execution?
Scope one collector to one function-scoped driver fixture and never use a shared module-level list. Create unique artifact paths from the test node ID. Sanitize output and remove handlers before quitting the associated driver. With pytest-xdist, process isolation does not excuse module-level state, because one worker can still execute many tests. Function-scoped evidence makes attribution and retention predictable.
Q: What would you include in a browser-log allowlist?
I would include an exact stable signature, source or component scope, issue reference, owner, and expiry or review condition. I would not allowlist a generic level or phrase that could hide unrelated regressions. The allowlist is reviewed code, not a dumping ground. I run new rules in observation mode, measure recurring signatures, fix application-owned errors, and require an expiry for temporary exceptions. A generic phrase or severity-wide suppression is rejected.
Common Mistakes
- Setting
enable_bidiafterwebdriver.Chrome()has already created the session. - Adding a handler after navigation and expecting it to receive bootstrap messages from the past.
- Treating
console.errorand an uncaught JavaScript exception as identical signals. - Calling
get_log('browser')in both a helper and teardown, then wondering why one collection is empty. - Assuming Chromium logging capabilities are portable to every browser because the method exists on the Python driver.
- Using
time.sleep()instead of a specific bounded condition for asynchronous messages. - Keeping a global list that combines entries from parallel tests or multiple sessions.
- Failing on any message containing the word
errorwithout ownership and severity rules. - Logging complete messages, URLs, or stacks without redacting tokens and personal data.
- Letting teardown capture errors mask the original product assertion.
For additional interview practice around browser automation architecture, read Selenium interview questions for experienced QA engineers.
Conclusion
A reliable selenium browser console logs python implementation starts by choosing the right delivery model. WebDriver BiDi provides live typed console and JavaScript error callbacks, while Chromium get_log('browser') provides a straightforward buffer for established Chrome and Edge suites. Keep the collection mechanism behind a small adapter if the suite supports both. Tests can then consume one normalized evidence model while environment checks prove that the selected browser exposes the required feature.
Whichever route you use, install collection before the event, wait on a real condition, give one component ownership of evidence, and clean up within the driver lifecycle. Then add a narrow product-owned policy and secure artifact handling. The result is a useful diagnostic layer that strengthens feature tests without creating a second source of flaky failures. Start with one critical flow, capture in observation mode, and review the messages with front-end engineers. Convert only well-understood application-owned signals into failures, then add secure attachments and parallel isolation before expanding the policy across the suite. Re-run the smoke scenario on every supported browser and Grid image after dependency upgrades. If collection support changes, fail the environment preflight with capability details rather than letting product tests report misleading empty evidence. This keeps logging observability useful, explicit, and independently diagnosable.
Interview Questions and Answers
Describe the modern Selenium Python API for browser console messages.
I enable WebDriver BiDi on the options before session creation and register a callback with driver.script.add_console_message_handler. The callback receives typed entries, and registration returns an ID for removal. I install it before the action and keep collection scoped to one test.
How does get_log('browser') differ from a BiDi console handler?
get_log pulls buffered Chromium entries and drains what it returns. A BiDi handler receives live events through a callback while subscribed. BiDi also gives typed high-level console and JavaScript error objects and follows a standards-oriented direction.
How would you avoid losing buffered console entries?
I make one collector responsible for every read in the capture window and accumulate entries when polling. Assertions and reporters consume the saved collection instead of calling get_log independently. The window is labeled around a specific product action.
How would you design a pytest console policy?
I normalize evidence, filter by severity and application ownership, and return policy violations as data. Precise allowlist rules include an owner and issue reference. The fixture handles collection and cleanup, while unit tests validate the policy without launching a browser.
Why are fixed sleeps weak for console assertions?
They wait the same duration regardless of when the event arrives and still fail on slower environments. A bounded condition waits only until the expected message or product state exists. It also expresses the actual synchronization contract.
What security concerns apply to console artifacts?
Console text and stacks can contain tokens, signed URLs, user identifiers, and application state. I collect only necessary fields, redact known sensitive patterns, isolate artifacts by test, and follow retention controls. Full raw logging is never the default.
How do you keep a console listener safe in parallel execution?
Each driver fixture owns separate collections and handler IDs. No module-level mutable list is shared between workers or sessions. Artifact paths include a unique test identifier, and teardown removes only the handlers belonging to that driver.
Frequently Asked Questions
How do I get browser console logs in Selenium Python?
For live capture, enable BiDi and call driver.script.add_console_message_handler with a callback. For Chromium buffered logs, set the goog:loggingPrefs browser capability and call driver.get_log('browser').
Does driver.get_log('browser') clear the console log?
It returns the entries currently available and removes them from the next read window. Store the returned list if both an assertion and a reporter need the same evidence.
Why is driver.script unavailable in my Selenium Python test?
The session probably lacks a BiDi WebSocket URL. Set options.enable_bidi = True before creating the driver and confirm that the browser, driver, and Grid return a webSocketUrl capability.
Can Selenium Python capture uncaught JavaScript exceptions?
Yes. In a BiDi-enabled session, register driver.script.add_javascript_error_handler. The callback receives a JavaScript log entry with text and stack-trace information when available.
Do Selenium browser logs work in Firefox with Python?
WebDriver BiDi is designed for cross-browser implementations, but exact feature coverage must be tested. The goog:loggingPrefs and get_log browser workflow is primarily a Chromium approach and should not be assumed portable.
How should pytest tests wait for console messages?
Register the handler before the triggering action and use WebDriverWait with a predicate for the expected entry. For pull logs, either wait for the product completion state before reading or accumulate drained entries across bounded polls.
Should console log capture run for every Selenium test?
It can run as diagnostics, but enforcement should start with a focused policy and high-value flows. Keep collection per test, bound its size, sanitize it, and do not let capture failures replace the main test result.
Related Guides
- How to Use Selenium browser console logs in Java (2026)
- Selenium DevTools in Selenium 4 in Python (2026)
- How to Use Selenium DevTools in Selenium 4 in Java (2026)
- How to Debug a failing test in VS Code in Selenium (2026)
- How to Run tests in headed mode in Selenium (2026)
- Playwright Python vs Selenium Python (2026)